Skip to content

Commit 588b8ce

Browse files
authored
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
1 parent 70412cc commit 588b8ce

122 files changed

Lines changed: 786 additions & 498 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎.claude/rules/sim-components.md‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,15 +33,15 @@ export function Component({ requiredProp, optionalProp = false }: ComponentProps
3333
When rendering or sorting a list of rows against a lookup collection (members, folders, tags), keep the per-row work O(1):
3434

3535
- **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".
3737
- **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.
3838

3939
## react-doctor (`bunx react-doctor`) — apply the wins, skip the false positives
4040

4141
react-doctor diagnostics are hypotheses, not verdicts — confirm against the code before acting, and preserve behavior. Known repo-specific false positives to NOT "fix":
4242

4343
- `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.
44+
- `js-tosorted-immutable` — won't-fix anywhere; `check:utils` bans the ES2023 array methods repo-wide.
4545
- `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.
4646
- `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).
4747
- `async-await-in-loop` on an upload/progress loop where sequential execution is intentional (per-item progress, server backpressure) — leave it.

‎.claude/rules/sim-react-performance.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,7 @@ return items.sort(compare)
7777
return [...items].sort(compare)
7878
```
7979

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`).
8181

8282
## Run independent awaits in parallel
8383

‎.cursor/rules/sim-components.mdc‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,15 +34,15 @@ export function Component({ requiredProp, optionalProp = false }: ComponentProps
3434
When rendering or sorting a list of rows against a lookup collection (members, folders, tags), keep the per-row work O(1):
3535

3636
- **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".
3838
- **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.
3939

4040
## react-doctor (`bunx react-doctor`) — apply the wins, skip the false positives
4141

4242
react-doctor diagnostics are hypotheses, not verdicts — confirm against the code before acting, and preserve behavior. Known repo-specific false positives to NOT "fix":
4343

4444
- `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.
45+
- `js-tosorted-immutable` — won't-fix anywhere; `check:utils` bans the ES2023 array methods repo-wide.
4646
- `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.
4747
- `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).
4848
- `async-await-in-loop` on an upload/progress loop where sequential execution is intentional (per-item progress, server backpressure) — leave it.

‎.cursor/rules/sim-react-performance.mdc‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@ return items.sort(compare)
8080
return [...items].sort(compare)
8181
```
8282

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`).
8484

8585
## Run independent awaits in parallel
8686

‎CLAUDE.md‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ This file (also `AGENTS.md`) holds the repo-wide rules. Area detail lives in `.c
1010
- **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 })`.
1111
- **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.
1212
- **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):
1414
- `sleep(ms)` from `@sim/utils/helpers` — never `new Promise(resolve => setTimeout(resolve, ms))`
1515
- `toError(e)` from `@sim/utils/errors` — normalize caught values to `Error`; never `e instanceof Error ? e : new Error(String(e))`
1616
- `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
8989
- **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`.
9090
- **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)`).
9191
- **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.
9393
- **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`.
9494
- **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.
9595
- **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`.

‎apps/desktop/src/main/local-filesystem.ts‎

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ import {
1919
} from '@sim/desktop-bridge/local-filesystem-limits'
2020
import { generateId } from '@sim/utils/id'
2121
import { isRecordLike } from '@sim/utils/object'
22-
import { escapeRegExp } from '@sim/utils/string'
22+
import { escapeRegExp, truncate } from '@sim/utils/string'
2323
import { app, dialog, shell } from 'electron'
2424
import micromatch from 'micromatch'
2525
import safeRegex from 'safe-regex2'
@@ -1236,8 +1236,7 @@ export class LocalFilesystemService {
12361236
matches.push({
12371237
uri: resultUri,
12381238
line: request.lineNumbers === false ? 0 : contextIndex + 1,
1239-
text:
1240-
line.length > MAX_GREP_LINE_LENGTH ? `${line.slice(0, MAX_GREP_LINE_LENGTH)}…` : line,
1239+
text: truncate(line, MAX_GREP_LINE_LENGTH, '…'),
12411240
})
12421241
if (matches.length >= maxResults) {
12431242
truncated = true

‎apps/docs/app/api/search/route.ts‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
1+
import { createLogger } from '@sim/logger'
12
import { sql } from 'drizzle-orm'
23
import { type NextRequest, NextResponse } from 'next/server'
34
import { db, docsEmbeddings } from '@/lib/db'
45
import { generateSearchEmbedding } from '@/lib/embeddings'
56

7+
const logger = createLogger('DocsSearchAPI')
8+
69
export const runtime = 'nodejs'
710
export const revalidate = 0
811

@@ -196,7 +199,7 @@ export async function GET(request: NextRequest) {
196199

197200
return NextResponse.json(searchResults)
198201
} catch (error) {
199-
console.error('Semantic search error:', error)
202+
logger.error('Semantic search error:', error)
200203

201204
return NextResponse.json([])
202205
}

‎apps/docs/app/llms-full.txt/route.ts‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
1+
import { createLogger } from '@sim/logger'
12
import { getLLMText } from '@/lib/llms'
23
import { source } from '@/lib/source'
34

5+
const logger = createLogger('DocsLlmsFullText')
6+
47
export const revalidate = false
58

69
export async function GET() {
@@ -18,7 +21,7 @@ export async function GET() {
1821
},
1922
})
2023
} catch (error) {
21-
console.error('Error generating LLM full text:', error)
24+
logger.error('Error generating LLM full text:', error)
2225
return new Response('Error generating full documentation text', { status: 500 })
2326
}
2427
}

‎apps/docs/app/llms.txt/route.ts‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
1+
import { createLogger } from '@sim/logger'
12
import { source } from '@/lib/source'
23
import { DOCS_BASE_URL } from '@/lib/urls'
34

5+
const logger = createLogger('DocsLlmsManifest')
6+
47
export const revalidate = false
58

69
export async function GET() {
@@ -70,7 +73,7 @@ See: https://llmstxt.org for specification`
7073
},
7174
})
7275
} catch (error) {
73-
console.error('Error generating LLM manifest:', error)
76+
logger.error('Error generating LLM manifest:', error)
7477
return new Response('Error generating documentation manifest', { status: 500 })
7578
}
7679
}

‎apps/docs/components/ui/video-placeholder.tsx‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -113,7 +113,7 @@ export function VideoPlaceholder({
113113
pendingSeek.current = null
114114
}
115115
}}
116-
className='h-full w-full border-0'
116+
className='size-full border-0'
117117
>
118118
<track
119119
kind='captions'

0 commit comments

Comments
 (0)