Files
siftlode/docs/gates.md
T
peter 36653886d5 build(frontend)!: knip drops --production; plain knip stays
The production pass (7ed2ac4e, E32) asked "is this export used by what
ships?". No real hit: its only findings were 37 exports a test imports,
answered with 36 `@internal` JSDoc tags (E34 S2) — test-only exports, not
defects. Since then it has only ever asked for another tag. The tags go
with it (36, no longer read by anything), and so do the production-only
`!…!` exclusions in knip.jsonc and their register rows. Plain knip (unused
files, exports and dependencies) keeps running in the gate.
2026-10-05 00:19:38 +02:00

14 KiB
Raw Blame History

Gate register

Every place a gate deliberately checks less than it could: a rule switched off or narrowed for a scope, a flag left off, a lane that does not run everywhere, an accepted finding. One row each, with the reason and the number measured when the row was written.

Why this file exists (E32). The worst defect E32 found was not in any code a reviewer read: a lint rule had been switched off with 64 real hits counted in its own comment, and the "false positives" beside them were the React Compiler skipping 95 functions without a word. The rule since: a gate with any real hit is never switched off or narrowed — fix the sites, or suppress each one inline and name its invariant. Whatever is left narrowed is listed here, and frontend/src/lib/gates.contract.test.ts reads the effective configuration and fails when a narrowing is not on this page. A row here is a decision, not an oversight; a narrowing missing from here is a test failure.

The snapshot. ESLint's effective config for every code file eslint . reaches, and the lines that run the lanes (scripts/check.sh, backend/docker-compose.test.yml, backend/Dockerfile.test, the npm scripts, vitest.config.ts, .prettierignore), are committed in frontend/src/lib/gates.snapshot.json. The test fails on ANY difference — a rule, an option, a glob that moves one file to another config, a new ignore, a gate flag. So a change to a gate is always a diff of that file in review, never a shape a detector failed to model (E32 review: three rounds of a cleverer detector each found a bypass of the last one's fixes). Regenerate it with GATE_SNAPSHOT=update npx vitest run src/lib/gates.contract.test.ts (from frontend/), and read its diff as a gate change: a new file only adds its path; anything else needs a reason. A new code file is part of that diff by design.

How the register is matched: each narrowing must appear in a table row below as its code-quoted id, in a row whose Scope cell names the scope exactly as the test reports it (a cell may list several, separated by ·). For ESLint the scopes are the snapshot's config names, and a narrowing is read off each config against src: a rule at off or warn, or one src applies that a scope does not (one id per plugin family), a no-restricted-* list that leaves out an entry src's declares, an option that lets something through (allow…, ignore…, except…), an unused-disable report that is not an error, and a file no config lints. Ruff's TOML is parsed and held to a whitelist of key paths, pytest.ini to a whitelist of whole lines.

ESLint (frontend)

npm run lint is eslint . --max-warnings 0, so a rule at warn fails the lane exactly like an error (the test holds that exact script). The warn rows are listed so that claim stays checked. A core rule switched off because its typescript-eslint twin replaces it (no-unused-vars → @typescript-eslint/no-unused-vars) is not listed separately; neither are 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, …), nor the formatting rules eslint-config-prettier switches off (Prettier owns formatting and has its own lane).

Scope Narrowed Why Measured
src @typescript-eslint/no-unused-vars — off tsc owns it (noUnusedLocals + noUnusedParameters in every project), so a finding is reported once, by one tool. —
src react-hooks/exhaustive-deps — warn Preset level; --max-warnings 0 makes it fail. Every suppression names its invariant (eslint-comments/require-description). 0 suppressions (E32 S4: a suppression of this rule drops the whole function from the React Compiler, so the two that were left went)
src react-hooks/incompatible-library — warn Preset level; fails under --max-warnings 0. VirtualGrid (TanStack Virtual) is the one site, suppressed inline. 1
src react-hooks/unsupported-syntax — warn Preset level; fails under --max-warnings 0. 0
src react-refresh/only-export-components — warn Fails under --max-warnings 0; allowConstantExport lets a module export constants beside its component. 0
src @typescript-eslint/no-unused-expressions — narrowed allowShortCircuit + allowTernary: cond && f() / a ? f() : g() side-effect statements are an idiom here. —
src @typescript-eslint/no-floating-promises — narrowed allowForKnownSafeCalls: React Query's invalidateQueries, refetchQueries, cancelQueries, resetQueries, prefetchQuery, prefetchInfiniteQuery never reject unless asked to (throwOnError), so a bare call is safe by name. 6 names
e2e @typescript-eslint/no-floating-promises — narrowed The same allowForKnownSafeCalls list (the typed rules are one object for both scopes). 6 names
lib/http.ts no-restricted-globals — narrowed A network owner (NETWORK_OWNERS in eslint.config.js): the one module that may call fetch (send, which req and the public pages go through). Every other network global stays restricted there. 1 global
lib/api/core.ts no-restricted-globals — narrowed A network owner: uploadReq is the one XMLHttpRequest (upload progress). 1 global
src tests · ui tests · lib tests no-restricted-globals, no-restricted-properties — off A test stands a double in for the network (vi.stubGlobal("fetch", …)) and may reach it directly. —
src tests · ui tests · lib tests local/literal-a11y-text, local/button-primitive, local/input-primitive, local/surface-role, local/glyph-link — off A test renders throwaway markup (a raw button as a focus target, an untranslated label) that is not product UI. —
FocusSafeButton.tsx + AnchoredPopover.tsx local/focus-safe-disabled — off FocusSafeButton: it IS the focus-safe primitive the rule points everyone else to. —
FocusSafeButton.tsx + AnchoredPopover.tsx local/focus-safe-disabled — off AnchoredPopover: the popover's menu row: its native disabled is what floating-ui's roving focus reads to skip it. —
design-lab local/literal-a11y-text — off The dev-only design lab (never in the bundle — devOnlyGuard fails the build if it is) holds the owner's working copy, not product copy. —
e2e react-hooks/*, react-refresh/*, local/*, no-console, no-restricted-globals, no-restricted-properties, no-restricted-syntax — not applied Playwright specs drive a browser from Node: no React, no product UI, and they print to the console on purpose. They get the base + typed rules + the playwright rules. —
ts configs react-hooks/*, react-refresh/*, local/*, no-console, no-restricted-globals, no-restricted-properties, no-restricted-syntax — not applied The build configs (vite, vitest, playwright, reactPlugin, devOnlyGuard) run in Node at build time. —
js configs @typescript-eslint/*, react-hooks/*, react-refresh/*, local/*, no-console, no-restricted-globals, no-restricted-properties, no-restricted-syntax, no-var, prefer-const, prefer-rest-params, prefer-spread, no-array-constructor, no-unused-expressions — not applied eslint.config.js and postcss.config.js: plain-JS build config, linted with js.configs.recommended only. —
repo scripts scripts/*.mjs — not in the eslint or prettier lane; js.configs.recommended + Prettier via src/lib/repoScripts.contract.test.ts They sit at the repo root, and an ESLint config cannot reach past its own directory. The contract test lints and format-checks them from the repo root, to the same bar as the js configs row; no typed rules, as they are in no tsc project. 0 lint hits; 2 files reformatted (E32 review)
everywhere ignored paths: dist, node_modules, playwright-report, test-results, e2e/.auth Build output, installed packages, test artefacts, a live Playwright session. Generated, not written. —

Not enabled (decided, not forgotten): strictTypeChecked — owner decision E32 D3, ~2000 hits with none real in the sample; the curated typed pair no-floating-promises + no-deprecated runs instead.

TypeScript

Scope Narrowed Why Measured
config files eslint.config.js — not type-checked Linted (the js configs row) and executed by every lint run, but in no tsc project. Under the node project's strict + checkJs it reports 43 errors: 42 implicit-any parameters in the local rules' visitors (JSX nodes, which ESLint's own Rule typings do not model) and one inferred string severity. None is a defect; typing them means moving the local rules onto typescript-eslint's TSESTree types — a refactor of its own, not done in E32. 43 (E32 review)
all three projects skipLibCheck The .d.ts of installed packages are not type-checked (ours are). Standard for an app: a package's own types are its maintainers' gate, and checking them costs minutes and reports errors we cannot fix. —

Every project has strict, noUncheckedIndexedAccess, noUnusedLocals, noUnusedParameters and noFallthroughCasesInSwitch on; the test fails if one goes off, if a strict sub-flag (strictNullChecks, noImplicitAny, …) is set false beside it, if a project gains an extends, exclude or files, or if its include changes — and a config file at the frontend root that no project checks needs a row.

Ruff (backend + scripts)

Scope Narrowed Why Measured
backend + scripts B008 — narrowed: fastapi.Depends, fastapi.Query declared immutable FastAPI's dependency injection IS a call in a default argument, on every route. Every other call in a default still fails. 474 sites, all of them FastAPI
backend/app C901 — not in ruff; reported by scripts/audit_metrics.py Ruff has no per-function baseline, so turning it on would need a noqa per offender, and a noqa lets a function grow without limit. Complexity is a number an epic reads before and after (E35), not a gate: the ratchet that held it caught no defect in E32–E34, went red 8 times on its own ceilings and drew 5 review findings about itself. 32 functions over 10 (E34)

basedpyright (backend)

Scope Narrowed Why Measured
backend/app + alembic typeCheckingMode — standard Not strict/all: those report on every untyped third-party boundary (SQLAlchemy rows, yt-dlp dicts) — thousands of findings, near-zero real ones. standard was measured at 7 findings, all fixed (E32 S1). —
backend/app + alembic baseline.json — pre-existing errors absorbed The lane fails on NEW errors only; the baseline only shrinks (the lane fails when a run tightens it and it is not committed). Matching is per (file, rule, column span) + count, so burn the hot files first. 195 entries (E32 S1)
tests/ not type-checked A test's job is to fail loudly when wrong — the opposite of the silent runtime failure this lane catches — and annotating fixtures for a checker is churn. —

pytest (backend)

Scope Narrowed Why Measured
a hand run SIFTLODE_TEST_REQUIRE_DB=0 — the DB-backed and ffmpeg tests skip The explicit "pure-logic only" run for a machine without Postgres. Off by default since E32: without it a missing DB is a hard error; the gate and CI never set it. —

Lanes and pipeline

Scope Narrowed Why Measured
check e2e (Playwright) not in check It needs the :8080 stack built from the tree (minutes). It runs in check-full, and release rebuilds the stack and requires it (E32 D2) — nothing ships without it. 5 specs × 3 engines
pushes no server-side gate (no CI) Owner decision 2026-10-04. The gate (scripts/check.sh) runs on the dev machine before work is handed over, and release runs it, then e2e, before anything is merged and Trivy before the image is pushed — the CI only repeated that, after the merge. Its runner shared the git host's container, and one run there starved Gitea of memory until it stopped answering. What it added — a fresh npm ci on Linux — the image build does at publish. —
coverage no threshold Owner decision E32 D4: reported on both sides, never enforced. "Every regression test seen red" is the bar; a percentage invites tests written to move it. frontend 80% / backend 78% of lines (E32 S1)
CVE gate HIGH/CRITICAL with a fix available only --ignore-unfixed + --severity HIGH,CRITICAL: a finding with no fix cannot be acted on by an upgrade; it shows in the scan output. A fixable one fails the publish, and the only way past it is .trivyignore.yaml. 0 open (E32 S1)
npm audit --omit=dev --audit-level=high What ships to the browser, at high or above. The dev toolchain's advisories were burned down by hand in E32 S1 (18 → 0). 0
React Compiler the functions it skips, pinned components/compilerCoverage.contract.test.ts — the list may only shrink. End state: only VirtualGrid (TanStack Virtual, an IncompatibleLibrary) remains. 93 (E32 S1) → 29 (E32 S3) → 1, VirtualGrid (E32 S4)

Accepted findings

Entries in .trivyignore.yaml (CVE gate) and --ignore-vuln flags on the gate's pip-audit lane (scripts/check.sh). Each must have a row here (Scope accepted) with its reason and its expiry; the test fails on an entry without one, and on a .trivyignore.yaml entry with no statement or no expired_at.

Scope Finding Why Expires
— none — —