Skip to content

Commit c947017

Browse files
committed
improvement(audits): check rule frontmatter globs in check:guidance-refs
A paths glob that matches nothing silently stops the rule from loading. sim-api-contracts still targeted the removed apps/sim/hooks/selectors; drop it, and name check:api-validation:strict as the gate the rule describes.
1 parent 7cf0f33 commit c947017

12 files changed

Lines changed: 65 additions & 61 deletions

‎.claude/rules/sim-api-contracts.md‎

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ paths:
55
- "apps/sim/lib/api/**"
66
- "apps/sim/lib/**/application/**"
77
- "apps/sim/hooks/queries/**"
8-
- "apps/sim/hooks/selectors/**"
98
---
109

1110
# API Contracts and Routes
@@ -19,15 +18,15 @@ Boundary HTTP request and response shapes for all routes under `apps/sim/app/api
1918

2019
## Enforcement
2120

22-
`bun run check:api-validation` enforces boundary policy and prints ratchet metrics (route Zod imports, route-local schema constructors, route `ZodError` references, client hook Zod imports, and related counters). `bun run check:api-validation:strict` is the CI gate: it additionally fails on annotations with empty reasons. Both must pass on PRs.
21+
`bun run check:api-validation:strict` is the gate (it runs in `check:audits`): it enforces boundary policy, prints ratchet metrics (route Zod imports, route-local schema constructors, route `ZodError` references, client hook Zod imports), and fails on annotations with empty reasons. `check:api-validation` is the same audit without the strict reason check.
2322

2423
Whole-file allowlists for routes that legitimately import Zod for non-boundary reasons go through `INDIRECT_ZOD_ROUTES` in `scripts/check-api-validation-contracts.ts`, not per-line annotations.
2524

2625
## Boundary annotations
2726

2827
A small number of legitimate exceptions are tolerated when annotated. The audit recognizes four forms:
2928

30-
- `// boundary-raw-fetch: <reason>` — directly above a raw `fetch(` inside `apps/sim/hooks/queries/**`, `apps/sim/hooks/selectors/**`, or any other source under `apps/sim/**` (outside an API route handler) that targets a same-origin `/api/...` URL. Only for streaming responses, binary downloads, multipart uploads, signed-URL flows, OAuth redirects, and external-origin requests.
29+
- `// boundary-raw-fetch: <reason>` — directly above a raw `fetch(` inside `apps/sim/hooks/queries/**` or any other source under `apps/sim/**` (outside an API route handler) that targets a same-origin `/api/...` URL. Only for streaming responses, binary downloads, multipart uploads, signed-URL flows, OAuth redirects, and external-origin requests.
3130
- `// double-cast-allowed: <reason>` — directly above an `as unknown as X` cast outside test files.
3231
- `// boundary-raw-json: <reason>` — directly above a raw `await request.json()` / `await req.json()` read (or the multi-line `await request.clone().json()` shim variant) in a route handler. Only when the body is a JSON-RPC envelope, a tolerant `.catch(() => ({}))` parse, or otherwise cannot go through `parseRequest`.
3332
- `// untyped-response: <reason>` — directly above a `schema: z.unknown()` / `schema: z.object({}).passthrough()` / `schema: z.record(z.string(), z.unknown())` response declaration (or a simple alias to one of those) in a contract file. Only when the response body is genuinely opaque (user-supplied data, third-party passthrough).

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

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ The menu surface intentionally diverges from the pill: `dropdown-menu.tsx` items
2222

2323
## Component catalogue
2424

25-
- **`Chip` / `ChipLink`** — the pill button (`<button>` / Next `<Link>`). Variants: `primary`, `destructive`, `border-shadow`, `border`, `outline` (a true `--border` border, no shadow or hover fill); the bare chip is implicit (omit `variant`). `filled` is deliberately NOT a `Chip` variant — it is reserved for chip fields/triggers. For a selected/toggle chip use the `active` prop, never a variant. `leftIcon`/`rightIcon`, `active`, `fullWidth`. Chips carry **no outer margin** — space between them is the parent's `gap`. The old `mx-0.5` default and its `flush` opt-out are gone; do not reintroduce either, and never add a margin to a chip through `className`.
25+
- **`Chip` / `ChipLink`** — the pill button (`<button>` / Next `<Link>`). Variants: `primary`, `destructive`, `border-shadow`, `border`, `outline` (a true `--border` border, no shadow or hover fill); the bare chip is implicit (omit `variant`). `filled` is deliberately NOT a `Chip` variant — it is reserved for chip fields/triggers. For a selected/toggle chip use the `active` prop, never a variant. `leftIcon`/`rightIcon`, `active`, `fullWidth`. Chips carry **no outer margin** — space between them is the parent's `gap`, so never give a chip a margin, by default or through `className`.
2626
- **`ChipInput`** — single-line text field. `icon`, `endAdornment`, `error`, `inputClassName` (inner `<input>`); `className` styles the chrome wrapper.
2727
- **`ChipCopyInput`** — the canonical view-only field: a read-only `ChipInput` at full opacity with a trailing copy-to-clipboard button. View-only is a display mode, not a disabled state — reach for it (or `ChipModalField type='copy'`) over a `disabled` (greyed) input for values the user cannot edit.
2828
- **`ChipTextarea`** — multi-line sibling. `error`, `resizable` (off by default), `viewOnly` (read-only at full opacity with the default cursor — the multi-line counterpart of `ChipCopyInput`).
@@ -53,8 +53,8 @@ Declare keyboard intent on the action-owning primitive; never add document-level
5353
- **Props over `className` overrides.** When a consumer needs to change chrome, expose a prop (`error`, `icon`, `endAdornment`, `inputClassName`); reaching for `className` to restyle chrome is the smell.
5454
- **`cn()` for a single state toggle, CVA for genuine multiple variants.** A lone `error` boolean is `cn()`, not a CVA variant.
5555
- **Discriminated-union props for modes** (e.g. `multiple`, the modal field `type`) instead of near-duplicate components.
56-
- **Delete legacy variants after migration** — don't leave dead paths (this paradigm removed `Input variant='chip'` and `ChipMultiSelect`).
57-
- **Verify CSS vars exist.** An undefined var resolves to `currentColor` (caused a real black-border bug). Align to the canonical tokens: normal weight, `--text-body`, `--text-icon`.
56+
- **Delete legacy variants after migration** — leave no dead paths.
57+
- **Verify CSS vars exist.** An undefined var resolves to `currentColor`, which draws black borders. Align to the canonical tokens: normal weight, `--text-body`, `--text-icon`.
5858
- Use Radix UI primitives for accessibility. Export the component and its `variants` (when using CVA). Document with TSDoc + a usage example.
5959

6060
Color tokens and icon-size conventions are canonical in `.claude/rules/sim-styling.md` — follow it rather than restating.

‎.cursor/rules/sim-api-contracts.mdc‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
---
22
description: "API contracts, route builders, boundary annotations, and the requestJson client boundary"
3-
globs: ["apps/sim/app/api/**","apps/sim/lib/api/**","apps/sim/lib/**/application/**","apps/sim/hooks/queries/**","apps/sim/hooks/selectors/**"]
3+
globs: ["apps/sim/app/api/**","apps/sim/lib/api/**","apps/sim/lib/**/application/**","apps/sim/hooks/queries/**"]
44
---
55

66
<!-- Generated from .claude/rules/sim-api-contracts.md by `bun run skills:sync`. Edit the source, not this file. -->
@@ -16,15 +16,15 @@ Boundary HTTP request and response shapes for all routes under `apps/sim/app/api
1616

1717
## Enforcement
1818

19-
`bun run check:api-validation` enforces boundary policy and prints ratchet metrics (route Zod imports, route-local schema constructors, route `ZodError` references, client hook Zod imports, and related counters). `bun run check:api-validation:strict` is the CI gate: it additionally fails on annotations with empty reasons. Both must pass on PRs.
19+
`bun run check:api-validation:strict` is the gate (it runs in `check:audits`): it enforces boundary policy, prints ratchet metrics (route Zod imports, route-local schema constructors, route `ZodError` references, client hook Zod imports), and fails on annotations with empty reasons. `check:api-validation` is the same audit without the strict reason check.
2020

2121
Whole-file allowlists for routes that legitimately import Zod for non-boundary reasons go through `INDIRECT_ZOD_ROUTES` in `scripts/check-api-validation-contracts.ts`, not per-line annotations.
2222

2323
## Boundary annotations
2424

2525
A small number of legitimate exceptions are tolerated when annotated. The audit recognizes four forms:
2626

27-
- `// boundary-raw-fetch: <reason>` — directly above a raw `fetch(` inside `apps/sim/hooks/queries/**`, `apps/sim/hooks/selectors/**`, or any other source under `apps/sim/**` (outside an API route handler) that targets a same-origin `/api/...` URL. Only for streaming responses, binary downloads, multipart uploads, signed-URL flows, OAuth redirects, and external-origin requests.
27+
- `// boundary-raw-fetch: <reason>` — directly above a raw `fetch(` inside `apps/sim/hooks/queries/**` or any other source under `apps/sim/**` (outside an API route handler) that targets a same-origin `/api/...` URL. Only for streaming responses, binary downloads, multipart uploads, signed-URL flows, OAuth redirects, and external-origin requests.
2828
- `// double-cast-allowed: <reason>` — directly above an `as unknown as X` cast outside test files.
2929
- `// boundary-raw-json: <reason>` — directly above a raw `await request.json()` / `await req.json()` read (or the multi-line `await request.clone().json()` shim variant) in a route handler. Only when the body is a JSON-RPC envelope, a tolerant `.catch(() => ({}))` parse, or otherwise cannot go through `parseRequest`.
3030
- `// untyped-response: <reason>` — directly above a `schema: z.unknown()` / `schema: z.object({}).passthrough()` / `schema: z.record(z.string(), z.unknown())` response declaration (or a simple alias to one of those) in a contract file. Only when the response body is genuinely opaque (user-supplied data, third-party passthrough).

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ Repository layout, package boundaries, the application operation boundary, namin
1111

1212
## The `'use client'` server boundary
1313

14-
Every export of a `'use client'` module becomes a *client reference* on the server — server-evaluated code (RSC pages/layouts, `prefetch.ts`, route handlers, block definitions, triggers) can only *render* it as a component or pass it as a prop, never *call* it (doing so throws at runtime, e.g. `tableKeys.list is not a function`; `next build` does not catch it). Keep server-importable query primitives (key factories, fetchers, mappers, constants) in non-`'use client'` modules — see `.claude/rules/sim-queries.md`. Enforced by `scripts/check-client-boundary-imports.ts`.
14+
Server-evaluated code (RSC pages/layouts, `prefetch.ts`, route handlers, block definitions, triggers) can render an export of a `'use client'` module but never call it; the call throws at runtime and `next build` does not catch it. Keep anything a server module calls (key factories, fetchers, mappers, constants) in a non-`'use client'` module. Detail and the escape hatch: `.claude/rules/sim-queries.md`. Enforced by `bun run check:client-boundary`.
1515

1616
## The app/worker runtime boundary
1717

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

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,8 +33,7 @@ export function Component({ requiredProp, optionalProp = false }: ComponentProps
3333

3434
When rendering or sorting a list of rows against a lookup collection (members, folders, tags), keep the per-row work O(1):
3535

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".
36+
- **Precompute a lookup `Map` once** (`useMemo` over the collection) and read `byId.get(id)` in the sort comparator, `.map(...)`, and cell builders. A `.find` inside a sort comparator is O(n²·log n). Depend downstream memos on the derived `Map`, not the raw array. Map-building and copy-then-sort rules: `sim-react-performance.md`.
3837
- **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.
3938

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

‎.cursor/rules/sim-list-ordering.mdc‎

Lines changed: 4 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,6 @@ globs: ["apps/sim/app/**/*.tsx","apps/sim/ee/**/*.tsx","apps/sim/components/**/*
99

1010
**A list orders itself the way the user already reads the same things somewhere else.** Dropdowns, context menus, tab strips, command palettes, and settings navs are all *second* presentations of a set the user has already seen — in a toolbar, in the settings nav, in a column-header row. When the second presentation reorders that set, the user re-reads it from scratch every time.
1111

12-
This is not a style preference. Order is the cheapest affordance a list has, and the only one that costs nothing to get right.
13-
1412
## The rule
1513

1614
Before writing a list of items, find where the user sees those same items *first*. That surface owns the order; your list mirrors it.
@@ -93,14 +91,10 @@ const hasDestructiveSection = (showLeave && onLeave) || showDelete
9391
{hasActionsAboveDestructive && hasDestructiveSection && <DropdownMenuSeparator />}
9492
```
9593

96-
This is the failure that put a dangling rule at the bottom of the logs row menu, where two
97-
unconditional separators sat above conditional items.
94+
Unconditional separators above conditional items leave a dangling rule at the bottom of the menu.
9895

99-
**Do not add a prop to move a rule.** The shared workflow context menu grew
100-
`groupNonDestructiveActions` and `separateNavigationAction` for this; between them they moved one
101-
separator for one caller, four of six branches were unreachable, and `separateNavigationAction`
102-
had no observable effect anywhere in the repo. Both are gone. A menu that wants different
103-
grouping wants the standard grouping.
96+
**Never add a prop to move a rule.** Per-caller grouping props multiply unreachable branches; a
97+
menu that wants different grouping wants the standard grouping.
10498

10599
## Encode the order once
106100

@@ -130,7 +124,7 @@ The most common way a canonical order gets silently defeated: emitting all items
130124
{groups.filter((g) => !FOLDERED.has(g.type)).map(renderFlat)}
131125

132126
// ✓ Good — one ordered pass; each entry picks its own rendering
133-
{entries.sort(byResourceMenuOrder).map((entry) =>
127+
{[...entries].sort(byResourceMenuOrder).map((entry) =>
134128
sectionByType.has(entry.type) ? renderTree(entry) : renderFlat(entry)
135129
)}
136130
```

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

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -141,7 +141,6 @@ const handler = useCallback(() => {
141141
// ✓ Good — omit from deps, mutate is stable
142142
const handler = useCallback(() => {
143143
createEntity.mutate(data)
144-
// eslint-disable-next-line react-hooks/exhaustive-deps
145144
}, [data])
146145
```
147146

‎.cursor/rules/sim-settings-pages.mdc‎

Lines changed: 4 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -110,17 +110,9 @@ Adding a new settings page:
110110

111111
## Text-scale tokens (no literal pixel sizes)
112112

113-
Settings pages never use a literal `text-[Npx]` class — always the named Tailwind
114-
scale token from the `@theme` block in `apps/sim/app/_styles/globals.css` (`text-micro`
115-
10px, `text-xs` 11px, `text-caption` 12px, `text-small` 13px, `text-sm` 14px
116-
[Tailwind default, unmodified], `text-base` 15px, `text-md` 16px, `text-lg` 18px
117-
[Tailwind default]). A literal size is either a straight rename to the equivalent
118-
token (if the pixel value matches one exactly) or a sign the page never migrated —
119-
grep `text-\[1[0-8]px\]` under `apps/sim/app/workspace/*/settings/**` and
120-
`apps/sim/ee/**` to find stragglers.
121-
122-
Watch `text-xs`: it is 11px here, so a "caption" written as `text-xs` is a pixel
123-
short. See `sim-styling.md` for the full scale.
113+
Settings pages never use a literal `text-[Npx]` class — always a named token from
114+
the scale in `sim-styling.md` ("Text Scale"). Find stragglers with
115+
`rg 'text-\[1[0-8]px\]' apps/sim/app/workspace apps/sim/ee`.
124116

125117
The two-line list row (title over a muted subtitle — a name + email, a tool name
126118
+ description, a server name + status) is **not something you build**: it is
@@ -399,4 +391,4 @@ A settings page is design-system-clean when:
399391
- [ ] Decorative trailing content is in `badge`, not `trailing`.
400392
- [ ] Labeled sections use `SettingsSection`; read-only fields use `SettingsField`; empty/loading/error use `SettingsEmptyState`.
401393
- [ ] Delete is a plain `id:'delete'` header action behind a `ChipConfirmModal`; `destructive` is reserved for bulk actions.
402-
- [ ] `tsc`, `biome`, and the page's tests pass.
394+
- [ ] The local gate in the root `CLAUDE.md` ("How your work is checked") passes.

0 commit comments

Comments
 (0)