diff --git a/CLAUDE.md b/CLAUDE.md
index 18fbf777..87ef68f4 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -21,7 +21,7 @@ green **before** any merge, release, or handing work to review.
- Full gate: `bash scripts/check.sh` (`lint` for the static lanes only); the private ops dispatcher's `siftlode check` runs the same script. Single lanes: [`docs/development.md`](docs/development.md) § The gate.
- **There is no CI** (owner, 2026-10-04): this gate, run here, is the only one — green before every hand-over, and `release` refuses to merge without it.
- **A gate with any real hit is never switched off or narrowed** — not a lint rule, a compiler flag, a lane or a release step. Fix the sites, or suppress each one inline naming its invariant (`-- why`, lint-enforced). What stays narrowed is in [`docs/gates.md`](docs/gates.md), and a contract test fails when a narrowing is missing from it. The effective ESLint config of every scope and the gate/lane lines are pinned in `frontend/src/lib/gates.snapshot.json`: any gate change fails until you regenerate it (`GATE_SNAPSHOT=update`, see gates.md) — then read that diff as a gate change. A comment saying "X is enforced by Y" is checked by running Y (E32: one such comment hid 95 functions the React Compiler skipped).
-- **The backend type lane runs in the test image, not on the host** — `requirements.lock` cannot be installed on Windows (uvloop), and a type checker without the real packages reports nonsense. It fails on NEW errors only; the pre-existing ones sit in `backend/.basedpyright/baseline.json` (no burn-down mandate: E34 found none of them a real bug). A run that fixed some rewrites it — commit that.
+- **The backend type lane runs in the test image, not on the host** — `requirements.lock` cannot be installed on Windows (uvloop), and a type checker without the real packages reports nonsense. It fails on NEW errors only; the pre-existing ones sit in `backend/.basedpyright/baseline.json` (no burn-down mandate, but none was ever triaged: a `reportOptional*` entry in code you touch may be a real bug). A run that fixed some rewrites it — commit that.
- New DB-backed test? It must actually run, not silently skip — assume `SIFTLODE_TEST_REQUIRE_DB=1`.
## Workflow
diff --git a/docs/gates.md b/docs/gates.md
index 9440c3ef..fc88f61e 100644
--- a/docs/gates.md
+++ b/docs/gates.md
@@ -94,14 +94,14 @@ project checks needs a row.
| 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. No burn-down mandate (E35): E34 read the 7 standard-mode findings and none was a real bug, so the entries go when their code is touched, not as work of their own. | 195 entries (E32 S1); 176 (E35) |
+| 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. No burn-down mandate (E35): the entries go when their code is touched, not as work of their own. None of them was ever triaged one by one (the 7 findings E34 read were the `standard` ones above, fixed and not in the baseline), so an entry is not known to be harmless — the `reportOptional*` ones (24 in E35) are the likeliest real bugs; read one before re-baselining code around it. | 195 entries (E32 S1); 176 (E35) |
| 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. | — |
+| 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 never sets it. | — |
## Lanes and pipeline
diff --git a/frontend/src/lib/buttonPrimitive.contract.test.ts b/frontend/src/lib/buttonPrimitive.contract.test.ts
index 9e74a016..7d5f69b3 100644
--- a/frontend/src/lib/buttonPrimitive.contract.test.ts
+++ b/frontend/src/lib/buttonPrimitive.contract.test.ts
@@ -1,6 +1,6 @@
import { describe, expect, it } from "vitest";
-import { eslint } from "./lintProbe.testkit";
+import { eslint, uiFiles } from "./lintProbe.testkit";
// `local/button-primitive` (eslint.config.js): one primitive per button family (docs/DESIGN.md).
// 268 hand-rolled buttons were swept onto ui/Button, IconButton, Pressable, Chip and
@@ -43,6 +43,37 @@ describe("a hand-rolled button", () => {
).toBe(0);
});
+ // The primitive list lives in the rule body, where neither the gate snapshot nor docs/gates.md
+ // sees it, so this holds it to exactly these files of ui/.
+ it("is allowed in exactly the control primitives of ui/", { timeout: 60_000 }, async () => {
+ const allowed: string[] = [];
+ for (const file of uiFiles())
+ if (
+ (await hits(
+ 'export const R = () => ;\n',
+ `src/components/ui/${file}`,
+ )) === 0
+ )
+ allowed.push(file);
+ expect(allowed).toEqual([
+ "AnchoredPopover.tsx",
+ "Button.tsx",
+ "Chip.tsx",
+ "Combobox.tsx",
+ "DragHandle.tsx",
+ "FilterSelect.tsx",
+ "FocusSafeButton.tsx",
+ "IconButton.tsx",
+ "Pressable.tsx",
+ "PriorityStepper.tsx",
+ "SegmentedToggle.tsx",
+ "SelectTrigger.tsx",
+ "SortControl.tsx",
+ "Tabs.tsx",
+ "form.tsx",
+ ]);
+ });
+
it("is refused spelled as an element with the button role; a component's role is its own", async () => {
expect(await hits('export const F = () =>
x
;\n')).toBe(1);
expect(await hits('export const G = () => x;\n')).toBe(1);
diff --git a/frontend/src/lib/focusSafe.contract.test.ts b/frontend/src/lib/focusSafe.contract.test.ts
index 21f272d4..c00a9588 100644
--- a/frontend/src/lib/focusSafe.contract.test.ts
+++ b/frontend/src/lib/focusSafe.contract.test.ts
@@ -1,6 +1,6 @@
import { describe, expect, it } from "vitest";
-import { eslint } from "./lintProbe.testkit";
+import { eslint, uiFiles } from "./lintProbe.testkit";
// `local/button-primitive`'s disabled check (eslint.config.js): a raw `