docs(gates): the compiler burn-down after E32 S3, and the two idioms that keep it

react-hooks/refs 245 -> 31 hits and 93 -> 29 skipped functions in the register and the eslint
comment. The canonical-patterns table gets the latest-value row (useEffectEvent; a handler or timer:
useLatest / useStableCallback, never a render-phase ref write) and the popover / scroll-fade row
(destructure the ref setter).
This commit is contained in:
2026-10-03 23:18:33 +02:00
parent 76358a0d77
commit a486c7521a
3 changed files with 40 additions and 36 deletions
+3 -2
View File
@@ -81,7 +81,7 @@ CQRS, a repository/DI framework, a router library, a generic CRUD-router factory
## Canonical patterns — when you have a choice, pick THIS
| Area | Use | Not |
| ------------------------------------ | ----------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------ |
| ------------------------------------ | ----------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------- |
| ORM | SQLAlchemy 2.0 `select()` | `db.query(...)` |
| Request body | a Pydantic model | `payload: dict` |
| Request SIZE | nothing — `bodylimit` caps every route (SEC-20); need more? add a ceiling to `ROUTE_MAX_BODY_BYTES` | `capped_body` on a route that does not read raw bytes itself |
@@ -109,8 +109,9 @@ CQRS, a repository/DI framework, a router library, a generic CRUD-router factory
| FE diagnostics | `lib/log.ts` (`log.info("event.code", msg, attrs)`); a filter change → `lib/filterLog.ts`, a player → `lib/mediaLog.ts` | `console.*` (lint-enforced); a value only a higher level may carry, logged unguarded |
| FE state that MIRRORS a prop/query | derive it, or hold a draft (`draft ?? server`) | a copy an effect re-syncs — that paints the stale value first |
| FE state reset on a key change | adjust it DURING render against a remembered key | `useEffect(() => setX(init), [key])` |
| FE effect that must not re-subscribe | `useEffectEvent` (React 19.2+), else a latest-value ref | an excused dep list asserting the same thing in prose |
| FE effect that must not re-subscribe | `useEffectEvent` (React 19.2+); a handler or timer: `useLatest` / `useStableCallback` | a render-phase `ref.current = x` — the React Compiler skips the component |
| FE hook suppression | an inline `eslint-disable-next-line` NAMING the invariant | `eslint-suppressions.json` (deliberately absent — E27 S2) |
| FE popover / scroll-fade ref | destructure it: `const { setReference } = pop.refs`, then `ref={setReference}` | `ref={pop.refs.setReference}` / `ref={fade.ref}` — the compiler then treats the whole object as a ref and skips |
## House rules that bite (learned from real bugs)
+2 -2
View File
@@ -28,7 +28,7 @@ them itself (`no-undef`, `no-redeclare`, `no-dupe-keys`, …), nor the formattin
| Scope | Narrowed | Why | Measured |
| --- | --- | --- | --- |
| src | `react-hooks/refs` — off | The one rule off while it has real hits. Its analysis IS the React Compiler's: every hit is a component the compiler skips (the popover engine's `getReferenceProps()` / `refs.setReference` passed during render, render-phase ref writes). Too many to fix in one sprint; until they are, `components/compilerCoverage.contract.test.ts` pins every skipped function (none may be added) and E32 S3+ burns them down to turn this on. | 245 hits; 93 skipped functions (E32 S1) |
| src | `react-hooks/refs` — off | The one rule off while it has real hits. Its analysis IS the React Compiler's: every hit is a component the compiler skips (the popover engine's `getReferenceProps()` / `refs.setReference` passed during render, render-phase ref writes). Too many to fix in one sprint; until they are, `components/compilerCoverage.contract.test.ts` pins every skipped function (none may be added) and E32 burns them down to turn this on. S3 fixed the popover engine and every render-phase ref write; what is left sits in PlexPlayer and PlayerModal, behind the try/finally and suppression skips. | 245 hits, 93 skipped functions (E32 S1) → 31 hits (3 of them in tests), 29 skipped (E32 S3) |
| 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`). | 2 suppressions (both drop their whole function from the compiler — E32 S3) |
| src | `react-hooks/incompatible-library` — warn | Preset level; fails under `--max-warnings 0`. VirtualGrid (TanStack Virtual) is the one site, suppressed inline. | 1 |
@@ -96,7 +96,7 @@ Every project has `strict`, `noUncheckedIndexedAccess`, `noUnusedLocals`, `noUnu
| 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 | 93 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) |
| 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) |
| 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. | files >800 lines: 13 FE / 11 BE; functions over the limit: 64 FE / 32 BE |
## Accepted findings
+6 -3
View File
@@ -575,9 +575,12 @@ export default tseslint.config(
// and the other ~64 real render-phase `someRef.current = x` writes. The E32 audit showed the
// triage was wrong: the rule and the React Compiler share one analysis, so every one of those
// "false positives" is a component the compiler SKIPS — 21 popover components among them, and
// Channels' 0.9 s row menu was the bill. It stays off only because 245 hits (measured E32 S1) cannot be fixed in
// one sprint; until they are, compilerCoverage.contract.test.ts pins every skipped function
// (none may be added), and E32 S3+ burns them down to turn this back on.
// Channels' 0.9 s row menu was the bill. It stays off only because 245 hits (measured E32 S1)
// could not be fixed in one sprint. E32 S3 took them to 31: the popover engine is read through
// destructuring (`const { setReference } = pop.refs`), every render-phase write moved to
// `useEffectEvent` / `useLatest`. The rest sit in PlexPlayer and PlayerModal, behind the
// try/finally and suppression skips; until they are gone, compilerCoverage.contract.test.ts pins
// every skipped function (none may be added), and this comes back on.
"react-hooks/refs": "off",
"local/jsx-comment-text": "error",
"local/focus-safe-disabled": "error",