You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
improvement(audits): enforce console, helper, render-path, persist, and deployment-flag rules (#8559)
* chore(lint): ban console in runtime code with Biome noConsole
Runtime code logs through createLogger from @sim/logger. Scripts, CLIs,
script-migrations, the logger itself, SDK examples, and tests keep console
as their interface. Autofix is disabled so lint --unsafe never silently
deletes a console call.
* refactor(utils): replace inline toError, isRecordLike, omit, and truncate idioms with @sim/utils helpers
* refactor(ui): lazy-init object refs and use size-* for equal height and width
useRef(new X()) built a throwaway X on every render; the refs now
lazy-init through ??= as sim-react-performance.md prescribes. Equal
h-N w-N pairs become size-N per sim-styling.md.
* improvement(audits): extend check:utils to the remaining written idioms
Adds toError, isRecordLike, filterUndefined, omit, truncate, and
escapeRegExp idioms from CLAUDE.md, plus render-path rules: ES2023
array methods in browser code, useRef(new X()), and h-N w-N pairs.
* improvement(audits): require an explicit partialize on every zustand persist
sim-stores.md requires persist to whitelist durable fields; check:zustand-v5
now fails on a persist with no partialize or one that spreads the whole
state. canvas-mode was the one store without it.
* improvement(audits): flag deployment-shape env-flags imports in client settings surfaces
check:client-boundary now fails when a 'use client' module under the
workspace, organization, or standalone settings surfaces imports isHosted,
isBillingEnabled, isChatEnabled, or an enterprise feature flag from
env-flags instead of reading the seeded deployment shape.
* docs(agents): name the check that enforces the common-utilities list
* docs(agents): scope the check:utils note to the forms it bans
* perf(audits): gate backreference patterns in check:utils behind literal prefilters
The h-N/w-N, toError, and truncate patterns backtrack from every word
boundary; a cheap literal test per file keeps the scan at ~1s of CPU.
* refactor(ui): lazy-init useRef containers the nested-generic pattern missed
Allocate Map/Set ref containers once instead of on every render, and drop the
redundant processedRemovalIds alias in the toast provider.
* improvement(audits): close detector gaps in check:utils and the deployment-shape rule
- check:utils: match useRef(new X()) with nested generics, honor utils-lint-allow
above formatter-wrapped statements, drop h-screen/w-screen from the size-N rule,
and skip server-only App Router files in the ES2023 rule
- deployment-shape rule: cover stores/, hooks/, blocks/ and surface hooks, read
namespace imports, derive the flag list from deployment-shape.ts, parse long
import clauses whole, and allowlist the panel store's module-init isChatEnabled
- zustand persist message names the hoisted-options escape
- biome: allow console in *.integration.ts, *.spec.ts, and desktop e2e
* improvement(audits): share the directive classifier and simplify check:utils
- move leadingDirective/directiveOn into scripts/source-kind.ts; check:utils uses it
instead of its own 'use client' regex, and multi-line block-comment headers now parse
- replace the deployment-shape allowlist with a client-boundary-allow annotation on
the panel store's isChatEnabled import
- exempt all of packages/utils/src by prefix (drops the stale retry.test.ts entry)
- build both truncate patterns from one shared fragment and prefilter
- add literal prefilters to isRecordLike, fromEntries, and useRef patterns and
memoize prefilter results per file
- trim the deployment-shape rationale to a CLAUDE.md pointer
* refactor: drop isRecordLike pass-through wrappers and return audioLevels directly
useSpeechToText returns its stable, in-place-filled Float32Array instead of a
nullable ref; MicButton and the composer, search, and user-input props follow.
* improvement(audits): ban ES2023 array methods repo-wide, catch whole-state partialize, strip inline directive comments
* improvement(audits): follow aliased persist imports, require strict !== for filterUndefined, state the .with scope
When rendering or sorting a list of rows against a lookup collection (members, folders, tags), keep the per-row work O(1):
34
34
35
35
-**Precompute a lookup `Map` once**, never `array.find(...)` per row. Build `const byId = useMemo(() => { const m = new Map<string, T>(); for (const x of items ?? []) m.set(x.id, x); return m }, [items])` and read `byId.get(id)` in the sort comparator, `.map(...)`, and cell builders. A `.find` inside a sort comparator is O(n²·log n) — the worst offender. Depend memos on the derived `Map`, not the raw array.
36
-
-**Sort a copy with `[...array].sort(cmp)`**, never `toSorted`in client code — see `sim-react-performance.md` → "Never mutate a shared array in place".
36
+
-**Sort a copy with `[...array].sort(cmp)`**, never `toSorted`(`check:utils` bans it repo-wide) — see `sim-react-performance.md` → "Never mutate a shared array in place".
37
37
-**Partition in a single pass** — when splitting one collection into several (`fileIds`/`folderIds`), do one `for…of` pushing into each bucket and return `{ a, b }` from a single `useMemo`, not two memos that each `map→filter→map` the same source twice.
38
38
39
39
## react-doctor (`bunx react-doctor`) — apply the wins, skip the false positives
40
40
41
41
react-doctor diagnostics are hypotheses, not verdicts — confirm against the code before acting, and preserve behavior. Known repo-specific false positives to NOT "fix":
42
42
43
43
-`no-barrel-import` — barrel imports are the repo convention (see sim-imports.md, "Barrel Exports"). Keep them.
44
-
-`js-tosorted-immutable` — won't-fix in `'use client'` code (see "List-render performance" above); apply it only in server-only modules.
-`rerender-state-only-in-handlers` / "state set but never rendered" — a false positive when the `useState` is consumed by a `useEffect`/`useLayoutEffect` dependency (the effect must re-run on change). Only convert to a ref when nothing reads the value reactively.
46
46
-`no-render-in-render` — a helper *called inline* (`{renderRow()}`) is reconciled by position and does **not** remount, so extracting it to a component is usually pure churn and can regress behavior (prop-drilling many closures, focus/scroll loss on the inner `<input>`). Apply it only when the helper is genuinely a *component defined during render*, or when the move is mechanical (a stateless, ref-free helper whose closures become a small, explicit prop set).
47
47
-`async-await-in-loop` on an upload/progress loop where sequential execution is intentional (per-item progress, server backpressure) — leave it.
Copy file name to clipboardExpand all lines: .claude/rules/sim-react-performance.md
+1-1Lines changed: 1 addition & 1 deletion
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -77,7 +77,7 @@ return items.sort(compare)
77
77
return [...items].sort(compare)
78
78
```
79
79
80
-
**Do NOT reach for `toSorted()` / `toReversed()` / `with()` / `toSpliced()` on client render paths.** They are ES2023 *runtime* methods — and a tsconfig `"lib": ["ES2023"]` only makes them **type-check**, it does not make them **run**. Next/SWC compiles syntax but does **not** polyfill prototype methods, and the default browserslist still includes browsers without them (`toSorted` landed in Safari 16 / iOS 16, so any device capped at iOS 15 throws `TypeError: x.toSorted is not a function` and crashes the page). The perf difference vs `[...arr].sort()` is negligible (both allocate one array), so the copy-then-sort form is the correct default everywhere client code runs. Only consider the immutable methods in Node-only code (server routes, scripts) on Node ≥20, where the runtime is known.
80
+
**Do NOT use `toSorted()` / `toReversed()` / `with()` / `toSpliced()`.** They are ES2023 *runtime* methods — and a tsconfig `"lib": ["ES2023"]` only makes them **type-check**, it does not make them **run**. Next/SWC compiles syntax but does **not** polyfill prototype methods, and the default browserslist still includes browsers without them (`toSorted` landed in Safari 16 / iOS 16, so any device capped at iOS 15 throws `TypeError: x.toSorted is not a function` and crashes the page). The perf difference vs `[...arr].sort()` is negligible (both allocate one array), so the copy-then-sort form is used everywhere: whether a module reaches the browser is not visible from its path. `check:utils` flags `toSorted`/`toReversed`/`toSpliced` repo-wide and `with` when called with a numeric index (an identifier index is indistinguishable from OpenTelemetry's `context.with`).
When rendering or sorting a list of rows against a lookup collection (members, folders, tags), keep the per-row work O(1):
35
35
36
36
- **Precompute a lookup `Map` once**, never `array.find(...)` per row. Build `const byId = useMemo(() => { const m = new Map<string, T>(); for (const x of items ?? []) m.set(x.id, x); return m }, [items])` and read `byId.get(id)` in the sort comparator, `.map(...)`, and cell builders. A `.find` inside a sort comparator is O(n²·log n) — the worst offender. Depend memos on the derived `Map`, not the raw array.
37
-
- **Sort a copy with `[...array].sort(cmp)`**, never `toSorted` in client code — see `sim-react-performance.md` → "Never mutate a shared array in place".
37
+
- **Sort a copy with `[...array].sort(cmp)`**, never `toSorted` (`check:utils` bans it repo-wide) — see `sim-react-performance.md` → "Never mutate a shared array in place".
38
38
- **Partition in a single pass** — when splitting one collection into several (`fileIds`/`folderIds`), do one `for…of` pushing into each bucket and return `{ a, b }` from a single `useMemo`, not two memos that each `map→filter→map` the same source twice.
39
39
40
40
## react-doctor (`bunx react-doctor`) — apply the wins, skip the false positives
41
41
42
42
react-doctor diagnostics are hypotheses, not verdicts — confirm against the code before acting, and preserve behavior. Known repo-specific false positives to NOT "fix":
43
43
44
44
- `no-barrel-import` — barrel imports are the repo convention (see sim-imports.md, "Barrel Exports"). Keep them.
45
-
- `js-tosorted-immutable` — won't-fix in `'use client'` code (see "List-render performance" above); apply it only in server-only modules.
- `rerender-state-only-in-handlers` / "state set but never rendered" — a false positive when the `useState` is consumed by a `useEffect`/`useLayoutEffect` dependency (the effect must re-run on change). Only convert to a ref when nothing reads the value reactively.
47
47
- `no-render-in-render` — a helper *called inline* (`{renderRow()}`) is reconciled by position and does **not** remount, so extracting it to a component is usually pure churn and can regress behavior (prop-drilling many closures, focus/scroll loss on the inner `<input>`). Apply it only when the helper is genuinely a *component defined during render*, or when the move is mechanical (a stateless, ref-free helper whose closures become a small, explicit prop set).
48
48
- `async-await-in-loop` on an upload/progress loop where sequential execution is intentional (per-item progress, server backpressure) — leave it.
Copy file name to clipboardExpand all lines: .cursor/rules/sim-react-performance.mdc
+1-1Lines changed: 1 addition & 1 deletion
Original file line number
Diff line number
Diff line change
@@ -80,7 +80,7 @@ return items.sort(compare)
80
80
return [...items].sort(compare)
81
81
```
82
82
83
-
**Do NOT reach for `toSorted()` / `toReversed()` / `with()` / `toSpliced()` on client render paths.** They are ES2023 *runtime* methods — and a tsconfig `"lib": ["ES2023"]` only makes them **type-check**, it does not make them **run**. Next/SWC compiles syntax but does **not** polyfill prototype methods, and the default browserslist still includes browsers without them (`toSorted` landed in Safari 16 / iOS 16, so any device capped at iOS 15 throws `TypeError: x.toSorted is not a function` and crashes the page). The perf difference vs `[...arr].sort()` is negligible (both allocate one array), so the copy-then-sort form is the correct default everywhere client code runs. Only consider the immutable methods in Node-only code (server routes, scripts) on Node ≥20, where the runtime is known.
83
+
**Do NOT use `toSorted()` / `toReversed()` / `with()` / `toSpliced()`.** They are ES2023 *runtime* methods — and a tsconfig `"lib": ["ES2023"]` only makes them **type-check**, it does not make them **run**. Next/SWC compiles syntax but does **not** polyfill prototype methods, and the default browserslist still includes browsers without them (`toSorted` landed in Safari 16 / iOS 16, so any device capped at iOS 15 throws `TypeError: x.toSorted is not a function` and crashes the page). The perf difference vs `[...arr].sort()` is negligible (both allocate one array), so the copy-then-sort form is used everywhere: whether a module reaches the browser is not visible from its path. `check:utils` flags `toSorted`/`toReversed`/`toSpliced` repo-wide and `with` when called with a numeric index (an identifier index is indistinguishable from OpenTelemetry's `context.with`).
Copy file name to clipboardExpand all lines: CLAUDE.md
+2-2Lines changed: 2 additions & 2 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -10,7 +10,7 @@ This file (also `AGENTS.md`) holds the repo-wide rules. Area detail lives in `.c
10
10
-**Logging**: `createLogger` from `@sim/logger`; `logger.info` / `logger.warn` / `logger.error`, never `console.log`. Inside `withRouteHandler` the logger already carries the request ID — no manual `withMetadata({ requestId })`.
11
11
-**Comments**: TSDoc for documentation. An inline `//` only for a terse, non-obvious why, or for a script-enforced `// <tag>: <reason>` annotation (`boundary-raw-fetch`, `double-cast-allowed`, `boundary-raw-json`, `untyped-response`, `rq-lint-allow`, `client-boundary-allow`, …). No `====` separators.
12
12
-**ID generation**: `generateId()` (UUID v4, the default) or `generateShortId(size?)` (URL-safe, 21 chars by default) from `@sim/utils/id` — never `crypto.randomUUID()`, `nanoid`, or `uuid`. Both use `crypto.getRandomValues()`, so they also work in non-secure (HTTP) browsers.
13
-
-**Common utilities**: use the shared helpers from `@sim/utils` instead of inline implementations:
13
+
-**Common utilities**: use the shared helpers from `@sim/utils` instead of inline implementations (`check:utils` bans most of the inline forms below):
14
14
-`sleep(ms)` from `@sim/utils/helpers` — never `new Promise(resolve => setTimeout(resolve, ms))`
15
15
-`toError(e)` from `@sim/utils/errors` — normalize caught values to `Error`; never `e instanceof Error ? e : new Error(String(e))`
16
16
-`getErrorMessage(e, fallback?)` from `@sim/utils/errors` — never `e instanceof Error ? e.message : 'fallback'`
@@ -89,7 +89,7 @@ The `'use client'` server boundary, the app/worker runtime env split, and featur
89
89
-**Imports**: absolute (`@/...`) only, never relative. A folder with 3+ exports gets an `index.ts` barrel; never re-export from a non-barrel file. `import type` for type-only imports. Order and lazy-loading through barrels: `.claude/rules/sim-imports.md`.
90
90
-**TypeScript**: no `any` and no non-null `!` (use precise types or `unknown` with guards; `check:explicit-any` ratchets both); no export nothing imports (`check:unused-exports`); a props interface for every component; `as const` for constant objects/arrays; explicit ref types (`useRef<HTMLDivElement>(null)`).
91
91
-**Unused bindings** fail lint (biome `noUnusedVariables`, `noUnusedFunctionParameters`): delete the dead variable, import, or parameter and update callers; write `catch {}` when the error is unused. Prefix `_` only for a parameter that must hold its position because a later one is used. `const { a, ...rest } = obj` to omit keys is allowed. The rules carry no autofix, so `bun run lint` will not rename anything for you.
92
-
-**Components**: `'use client'` only for hooks or browser APIs. Structure order, extraction thresholds, and list-render rules: `.claude/rules/sim-components.md`. Render-performance idioms (lazy-init refs, hoisting, `Map` pre-indexing, `[...arr].sort()` never `toSorted()` on client paths): `.claude/rules/sim-react-performance.md`. For effect/state/memo/callback anti-patterns use the `/you-might-not-need-*` skills and verify against the running UI.
92
+
-**Components**: `'use client'` only for hooks or browser APIs. Structure order, extraction thresholds, and list-render rules: `.claude/rules/sim-components.md`. Render-performance idioms (lazy-init refs, hoisting, `Map` pre-indexing, `[...arr].sort()`, never `toSorted()`): `.claude/rules/sim-react-performance.md`. For effect/state/memo/callback anti-patterns use the `/you-might-not-need-*` skills and verify against the running UI.
93
93
-**State ownership**: React Query owns server data — never `useState` + `fetch`; shareable client view-state (tabs, filters, search, pagination, selected id) lives in the URL via `nuqs`; Zustand owns global client state; `useState` owns UI-only state. Hooks: `.claude/rules/sim-hooks.md`. Stores (`devtools`, `persist` only with an explicit `partialize` whitelist, workflow value invariants): `.claude/rules/sim-stores.md`. URL state: `.claude/rules/sim-url-state.md`.
94
94
-**Utils**: inline a helper with one consumer; create `utils.ts` when 2+ files share it — in `lib/` (app-wide) or `feature/utils/` (feature-scoped). Check `lib/` before writing a new one.
95
95
-**Lists and menus** mirror the order the user already reads elsewhere (toolbar, settings nav), encoded in one exported order constant (resource menus share `RESOURCE_MENU_ORDER`, a product order that does not mirror the sidebar); a separator marks only a change in what the action acts on (typically one, before the destructive action): `.claude/rules/sim-list-ordering.md`.
0 commit comments