ruff.toml ignored E402 everywhere for main.py's logging setup. Measured: 28 hits, of which 11 had nothing to do with it — runner.py's logger line between its imports, models.py importing canonical_email mid-file, a test constant above its imports, and main.py's own third-party and mimetypes imports. Those are moved up. What stays after configure_logging in main.py is marked per statement (noqa E402, logging is configured first), its comment now says why the order matters (a line logged while importing, not the logger's creation). The register row goes, the ruff key whitelist loses ignore, and a late import is red again. The T20 comment counts the third deliberate print (scripts/e2e_seed.py).
124 lines
14 KiB
Markdown
124 lines
14 KiB
Markdown
# 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`](../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.
|
||
|
||
How the test matches: 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. For ESLint it reads each
|
||
scope's effective config through a sample file AND every config block on its own: a block that
|
||
targets files no sample reaches fails as an `unsampled block` (add a sample), a rule a block or src
|
||
sets weaker than src counts, so does a `no-restricted-*` list that leaves out an entry src has, and
|
||
so does an option that lets something through (`allow…`, `ignore…`, `except…`) (E32 review).
|
||
|
||
## ESLint (frontend)
|
||
|
||
`npm run lint` is `eslint . --max-warnings 0`, so a rule at **warn** fails the lane exactly like an
|
||
error (the test checks the flag is still there). 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 |
|
||
| lib/messagesSocket.ts | `no-restricted-globals` — narrowed | A network owner: the chat socket is the one `WebSocket`. | 1 global |
|
||
| src 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 | `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 | `local/focus-safe-disabled` — off | It IS the focus-safe primitive the rule points everyone else to. | — |
|
||
| AnchoredPopover.tsx | `local/focus-safe-disabled` — off | 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; measured by `tests/test_source_size.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. The test holds each offender, by name, at its current value. Tests and scripts are outside it: see the size / complexity rows under Lanes. | 32 functions over 10 |
|
||
|
||
## 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. | — |
|
||
|
||
## knip (frontend)
|
||
|
||
| Scope | Narrowed | Why | Measured |
|
||
| --- | --- | --- | --- |
|
||
| --production | `!**/*.testkit.ts!`, `!e2e/**!` | Test kits and the e2e suite are test code: the production pass asks what the SHIPPED code uses. | — |
|
||
| --production | `!reactPlugin.ts!`, `!devOnlyGuard.ts!` | Build tooling the vite config imports: it shapes the bundle but is not in it. | — |
|
||
| --production | `@internal` exports | An export only its test imports says so with a JSDoc `@internal` tag naming the test; knip skips those in production mode. | 35 (E32 S1) |
|
||
|
||
## 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 |
|
||
| CI | no e2e, no Trivy | The lab runner has no browser stack, and Trivy's DB download is not reachable from it. Both run in `release` / `publish` on the dev machine, before anything is pushed. | — |
|
||
| 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 (CI) | `--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) |
|
||
| size / complexity | today's offenders as ceilings | `src/lib/sourceSize.contract.test.ts`, `backend/tests/test_source_size.py`: listed files and functions may not grow, nothing new may cross the limit, an entry back under it must be deleted. A function is held by its name, not its rank in the file. | files >800 lines: 13 FE / 9 BE; functions over the limit: 64 FE / 32 BE (E32 review) |
|
||
| size / complexity | shipped code only — not tests, test kits, e2e or `scripts/` | The ratchet guards what runs in production. A test file is long because it is a list of cases, and a test kit is test code; splitting either moves lines without making anything easier to change. `scripts/` is release and seed tooling, not in the image. | outside it: tests >800 lines 2 FE / 5 BE; functions over the limit 2 FE test code (`gates.contract.test.ts` 30, `compilerSkips.testkit.ts` 25), 1 BE test (11), 1 script (`e2e_seed.py` `main` 13) |
|
||
|
||
## Accepted findings
|
||
|
||
Entries in `.trivyignore.yaml` (CVE gate) and `--ignore-vuln` flags on CI's `pip-audit` step. 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 | — | — |
|