diff --git a/.agents/skills/db-migrate/SKILL.md b/.agents/skills/db-migrate/SKILL.md index 0ff4581a2b1..a2c05a96a86 100644 --- a/.agents/skills/db-migrate/SKILL.md +++ b/.agents/skills/db-migrate/SKILL.md @@ -83,7 +83,9 @@ The lint flags risky *shapes*; it cannot know whether a given drop is *safe righ ``` The reason must be specific and name the PR/version that removed the dependency. An empty reason fails the lint. - **Warnings** (`data-backfill`): non-blocking, but confirm the batching/idempotency before merging. -4. Verify locally: `cd packages/db && bun run db:migrate` against a dev DB. +4. Regenerate the test schema mock: `bun run scripts/generate-schema-mock.ts` (`check:schema-mock` in `check:audits` fails after any `schema.ts` change until you do). +5. Re-run `(cd packages/db && bunx drizzle-kit generate)` once your migration is written: it must report no schema changes and write no new file, or CI fails on the schema and migrations disagreeing. +6. Verify locally: `cd packages/db && bun run db:migrate` against a dev DB. ## Hard rule diff --git a/.claude/rules/constitution.md b/.claude/rules/constitution.md index 565dee4b877..3bd45448d59 100644 --- a/.claude/rules/constitution.md +++ b/.claude/rules/constitution.md @@ -1,5 +1,15 @@ --- description: Sim product language, positioning, and tone guidelines +paths: + - "apps/sim/app/(landing)/**" + - "apps/sim/lib/landing/**" + - "apps/sim/content/**" + - "apps/sim/emails/broadcasts/**" + - "apps/sim/app/layout.tsx" + - "apps/sim/app/manifest.ts" + - "apps/sim/app/llms*.txt/**" + - "apps/sim/app/changelog.xml/**" + - "apps/docs/**" --- # Sim — Language & Positioning diff --git a/.claude/rules/emcn-components.md b/.claude/rules/emcn-components.md index d4c697869d0..c21cad945cd 100644 --- a/.claude/rules/emcn-components.md +++ b/.claude/rules/emcn-components.md @@ -6,7 +6,7 @@ paths: # EMCN Components -Import components, `cn`, and tokens from the `@sim/emcn` barrel; icons come from the `@sim/emcn/icons` subpath, and CSS modules from their file path. Never deep-import other component subpaths. The **chip family** is the platform's primary chrome — always reach for it over the legacy primitives it is progressively replacing (`Input`→`ChipInput`, `Textarea`→`ChipTextarea`, `Modal`→`ChipModal`, `Select`/`Combobox`→`ChipSelect`/`ChipCombobox`/`ChipDropdown`, `Switch`→`ChipSwitch`, date field→`ChipDatePicker`). For context/action menus the canonical control is `DropdownMenu` — the standard menu (not a chip, and never a hand-rolled popover). +Import components, `cn`, and tokens from the `@sim/emcn` barrel; icons come from the `@sim/emcn/icons` subpath, and CSS modules from their file path. Never deep-import other component subpaths. The **chip family** is the platform's primary chrome — always reach for it over the legacy primitives it is progressively replacing (`Input`→`ChipInput`, `Textarea`→`ChipTextarea`, `Modal`→`ChipModal`, `Select`/`Combobox`→`ChipSelect`/`ChipCombobox`/`ChipDropdown`, date field→`ChipDatePicker`). `ChipSwitch` is a segmented choice between options, not a replacement for `Switch`: a boolean on/off toggle stays `Switch`. For context/action menus the canonical control is `DropdownMenu` — the standard menu (not a chip, and never a hand-rolled popover). ## Chip chrome — single source of truth diff --git a/.claude/rules/sim-queries.md b/.claude/rules/sim-queries.md index f09036b315c..3cf27da59e4 100644 --- a/.claude/rules/sim-queries.md +++ b/.claude/rules/sim-queries.md @@ -172,4 +172,4 @@ Hooks import named type aliases from `@/lib/api/contracts/**` and never import ` ## Enforcement -`scripts/check-react-query-patterns.ts` (`bun run check:react-query`, run in CI) statically enforces these conventions: every `useQuery`/`useInfiniteQuery`/`useSuspenseQuery` declares an explicit `staleTime`, inline `queryFn`s destructure `signal`, `queryKey`s reference a colocated factory rather than an inline literal, every `*Keys` factory in `hooks/queries/**` exposes an `all` root key, and every identifier the `queryFn` forwards into the fetch also appears in the `queryKey` (`key-fetch-arg-drift`). `hooks/queries/**` is a zero-tolerance zone; the rest of `apps/sim/**` is ratcheted against `scripts/check-react-query-patterns.baseline.json`. For a genuine exception, put `// rq-lint-allow: ` on the line directly above the flagged construct. +`scripts/check-react-query-patterns.ts` (`bun run check:react-query`, run in CI) statically enforces these conventions: every `useQuery`/`useInfiniteQuery`/`useSuspenseQuery` declares an explicit `staleTime`, inline `queryFn`s destructure `signal`, `queryKey`s reference a colocated factory rather than an inline literal, every `*Keys` factory in `hooks/queries/**` exposes an `all` root key, and every identifier the `queryFn` forwards into the fetch also appears in the `queryKey` (`key-fetch-arg-drift`). Any violation under `apps/sim/**` fails. For a genuine exception, put `// rq-lint-allow: ` on the line directly above the flagged construct. diff --git a/.claude/rules/sim-react-performance.md b/.claude/rules/sim-react-performance.md index f37fb447299..516cd46fc6a 100644 --- a/.claude/rules/sim-react-performance.md +++ b/.claude/rules/sim-react-performance.md @@ -1,5 +1,10 @@ --- description: Behavior-preserving React render-performance idioms +paths: + - "apps/sim/**/*.ts" + - "apps/sim/**/*.tsx" + - "packages/emcn/**" + - "packages/workflow-renderer/**" --- # React & Render Performance diff --git a/.claude/rules/sim-settings-pages.md b/.claude/rules/sim-settings-pages.md index 09393d6ef3b..40a6b5dcfba 100644 --- a/.claude/rules/sim-settings-pages.md +++ b/.claude/rules/sim-settings-pages.md @@ -224,8 +224,9 @@ and — on activatable rows only — the hover band. Never hand-roll any of it, divider, body. Also carries `headerAccessory` and `action` slots. Never re-derive the label/divider chrome; `sim-styling.md` owns those tokens. - **`SettingsField`** (`…/components/settings-field`) — a read-only label/value - pair in a detail body: muted caption over the value. Pair it with - `SETTINGS_FIELD_VALUE_CLASSES` for the value text. + pair in a detail body: muted caption over the value. Pass the value as text and it + renders the value paragraph itself; pass a node when the value needs its own + presentation (a control, an icon beside the value, status styling). - **`SettingsEmptyState`** (`…/components/settings-empty-state`) — the canonical muted status message, for empty lists, "no results", loading gates, **and failed loads** (`tone='error'`). `variant='fill'` (default) centers in the diff --git a/.claude/rules/sim-styling.md b/.claude/rules/sim-styling.md index f146e7a547c..e3cc5541e23 100644 --- a/.claude/rules/sim-styling.md +++ b/.claude/rules/sim-styling.md @@ -108,7 +108,7 @@ Layout/sizing ONLY: `flex-1`, `w-full`, `w-[Npx]`, `min-w-0`, `max-w-*`, margins - **Modal body** (`ChipModalBody`): `gap-4` between fields, padding `px-2 pt-4 pb-4.5`. - **Header/footer**: horizontal gutter `px-4` (header `pt-3`; footer `px-4 pt-2 pb-2`, tinted bar). - **Every body field MUST be a `ChipModalField`** — NEVER hand-roll a field row (raw `
` + hand-rolled `

`/`

`. -- **Uncovered controls** (`ChipCombobox`, `ChipSelect`, `DatePicker`, `TimePicker`, `ButtonGroup`, arbitrary JSX) → `ChipModalField type='custom'` with a `title`. It still applies the `px-2` gutter and renders the canonical `Label`, so it stays aligned. Never drop such a control into a raw `

`, and never add a body-level wrapper `
` with a custom `gap-*` that fights `gap-4`. +- **Uncovered controls** (`ChipCombobox`, `ChipSelect`, `ChipDatePicker`, `ChipTimePicker`, `ChipButtonGroup`, arbitrary JSX) → `ChipModalField type='custom'` with a `title`. It still applies the `px-2` gutter and renders the canonical `Label`, so it stays aligned. Never drop such a control into a raw `
`, and never add a body-level wrapper `
` with a custom `gap-*` that fights `gap-4`. - **Page section rhythm** (integrations/skills/settings): muted `text-small` label + `mt-[9px] mb-3 h-px bg-[var(--border)]` divider, sections stacked `gap-7`. Reuse `SettingsSection` (`app/workspace/[workspaceId]/settings/components/settings-section/settings-section.tsx`) rather than re-deriving it. When a standalone labeled field outside a `ChipModal` needs the same look (e.g. `SkillImport`), match the field rhythm by hand: `flex flex-col gap-[9px]`, muted label, `ChipInput`/`ChipTextarea` control, `text-caption` error below. diff --git a/.claude/rules/sim-url-state.md b/.claude/rules/sim-url-state.md index 6114efc83bc..914bb12403c 100644 --- a/.claude/rules/sim-url-state.md +++ b/.claude/rules/sim-url-state.md @@ -2,10 +2,14 @@ description: Shareable client view-state lives in the URL via nuqs paths: - "apps/sim/app/**/*.tsx" - - "apps/sim/app/**/*.ts" - - "apps/sim/app/**/search-params.ts" - - "apps/sim/ee/**/*.tsx" - - "apps/sim/ee/**/*.ts" + - "apps/sim/app/workspace/**/*.ts" + - "apps/sim/app/o/**/*.ts" + - "apps/sim/ee/**" + - "apps/sim/hooks/**" + - "apps/sim/stores/**" + - "apps/sim/lib/url-state/**" + - "apps/sim/**/search-params.ts" + - "apps/sim/**/*navigation.ts" --- # URL / Query-Param State (nuqs) diff --git a/.cursor/rules/constitution.mdc b/.cursor/rules/constitution.mdc index 9e9a50d2b2b..f97878570ab 100644 --- a/.cursor/rules/constitution.mdc +++ b/.cursor/rules/constitution.mdc @@ -1,6 +1,6 @@ --- description: "Sim product language, positioning, and tone guidelines" -alwaysApply: true +globs: ["apps/sim/app/(landing)/**","apps/sim/lib/landing/**","apps/sim/content/**","apps/sim/emails/broadcasts/**","apps/sim/app/layout.tsx","apps/sim/app/manifest.ts","apps/sim/app/llms*.txt/**","apps/sim/app/changelog.xml/**","apps/docs/**"] --- diff --git a/.cursor/rules/emcn-components.mdc b/.cursor/rules/emcn-components.mdc index b4743462057..d77a4082838 100644 --- a/.cursor/rules/emcn-components.mdc +++ b/.cursor/rules/emcn-components.mdc @@ -7,7 +7,7 @@ globs: ["packages/emcn/**"] # EMCN Components -Import components, `cn`, and tokens from the `@sim/emcn` barrel; icons come from the `@sim/emcn/icons` subpath, and CSS modules from their file path. Never deep-import other component subpaths. The **chip family** is the platform's primary chrome — always reach for it over the legacy primitives it is progressively replacing (`Input`→`ChipInput`, `Textarea`→`ChipTextarea`, `Modal`→`ChipModal`, `Select`/`Combobox`→`ChipSelect`/`ChipCombobox`/`ChipDropdown`, `Switch`→`ChipSwitch`, date field→`ChipDatePicker`). For context/action menus the canonical control is `DropdownMenu` — the standard menu (not a chip, and never a hand-rolled popover). +Import components, `cn`, and tokens from the `@sim/emcn` barrel; icons come from the `@sim/emcn/icons` subpath, and CSS modules from their file path. Never deep-import other component subpaths. The **chip family** is the platform's primary chrome — always reach for it over the legacy primitives it is progressively replacing (`Input`→`ChipInput`, `Textarea`→`ChipTextarea`, `Modal`→`ChipModal`, `Select`/`Combobox`→`ChipSelect`/`ChipCombobox`/`ChipDropdown`, date field→`ChipDatePicker`). `ChipSwitch` is a segmented choice between options, not a replacement for `Switch`: a boolean on/off toggle stays `Switch`. For context/action menus the canonical control is `DropdownMenu` — the standard menu (not a chip, and never a hand-rolled popover). ## Chip chrome — single source of truth diff --git a/.cursor/rules/sim-queries.mdc b/.cursor/rules/sim-queries.mdc index 82f695bc66b..e068cc48182 100644 --- a/.cursor/rules/sim-queries.mdc +++ b/.cursor/rules/sim-queries.mdc @@ -173,4 +173,4 @@ Hooks import named type aliases from `@/lib/api/contracts/**` and never import ` ## Enforcement -`scripts/check-react-query-patterns.ts` (`bun run check:react-query`, run in CI) statically enforces these conventions: every `useQuery`/`useInfiniteQuery`/`useSuspenseQuery` declares an explicit `staleTime`, inline `queryFn`s destructure `signal`, `queryKey`s reference a colocated factory rather than an inline literal, every `*Keys` factory in `hooks/queries/**` exposes an `all` root key, and every identifier the `queryFn` forwards into the fetch also appears in the `queryKey` (`key-fetch-arg-drift`). `hooks/queries/**` is a zero-tolerance zone; the rest of `apps/sim/**` is ratcheted against `scripts/check-react-query-patterns.baseline.json`. For a genuine exception, put `// rq-lint-allow: ` on the line directly above the flagged construct. +`scripts/check-react-query-patterns.ts` (`bun run check:react-query`, run in CI) statically enforces these conventions: every `useQuery`/`useInfiniteQuery`/`useSuspenseQuery` declares an explicit `staleTime`, inline `queryFn`s destructure `signal`, `queryKey`s reference a colocated factory rather than an inline literal, every `*Keys` factory in `hooks/queries/**` exposes an `all` root key, and every identifier the `queryFn` forwards into the fetch also appears in the `queryKey` (`key-fetch-arg-drift`). Any violation under `apps/sim/**` fails. For a genuine exception, put `// rq-lint-allow: ` on the line directly above the flagged construct. diff --git a/.cursor/rules/sim-react-performance.mdc b/.cursor/rules/sim-react-performance.mdc index 4ad3ca74971..98920fa7d2d 100644 --- a/.cursor/rules/sim-react-performance.mdc +++ b/.cursor/rules/sim-react-performance.mdc @@ -1,6 +1,6 @@ --- description: "Behavior-preserving React render-performance idioms" -alwaysApply: true +globs: ["apps/sim/**/*.ts","apps/sim/**/*.tsx","packages/emcn/**","packages/workflow-renderer/**"] --- diff --git a/.cursor/rules/sim-settings-pages.mdc b/.cursor/rules/sim-settings-pages.mdc index 98b9a684370..9f6959a04eb 100644 --- a/.cursor/rules/sim-settings-pages.mdc +++ b/.cursor/rules/sim-settings-pages.mdc @@ -221,8 +221,9 @@ and — on activatable rows only — the hover band. Never hand-roll any of it, divider, body. Also carries `headerAccessory` and `action` slots. Never re-derive the label/divider chrome; `sim-styling.md` owns those tokens. - **`SettingsField`** (`…/components/settings-field`) — a read-only label/value - pair in a detail body: muted caption over the value. Pair it with - `SETTINGS_FIELD_VALUE_CLASSES` for the value text. + pair in a detail body: muted caption over the value. Pass the value as text and it + renders the value paragraph itself; pass a node when the value needs its own + presentation (a control, an icon beside the value, status styling). - **`SettingsEmptyState`** (`…/components/settings-empty-state`) — the canonical muted status message, for empty lists, "no results", loading gates, **and failed loads** (`tone='error'`). `variant='fill'` (default) centers in the diff --git a/.cursor/rules/sim-styling.mdc b/.cursor/rules/sim-styling.mdc index 3cc6ebcd23b..359412b40c4 100644 --- a/.cursor/rules/sim-styling.mdc +++ b/.cursor/rules/sim-styling.mdc @@ -108,7 +108,7 @@ Layout/sizing ONLY: `flex-1`, `w-full`, `w-[Npx]`, `min-w-0`, `max-w-*`, margins - **Modal body** (`ChipModalBody`): `gap-4` between fields, padding `px-2 pt-4 pb-4.5`. - **Header/footer**: horizontal gutter `px-4` (header `pt-3`; footer `px-4 pt-2 pb-2`, tinted bar). - **Every body field MUST be a `ChipModalField`** — NEVER hand-roll a field row (raw `
` + hand-rolled `

`/`

`. -- **Uncovered controls** (`ChipCombobox`, `ChipSelect`, `DatePicker`, `TimePicker`, `ButtonGroup`, arbitrary JSX) → `ChipModalField type='custom'` with a `title`. It still applies the `px-2` gutter and renders the canonical `Label`, so it stays aligned. Never drop such a control into a raw `

`, and never add a body-level wrapper `
` with a custom `gap-*` that fights `gap-4`. +- **Uncovered controls** (`ChipCombobox`, `ChipSelect`, `ChipDatePicker`, `ChipTimePicker`, `ChipButtonGroup`, arbitrary JSX) → `ChipModalField type='custom'` with a `title`. It still applies the `px-2` gutter and renders the canonical `Label`, so it stays aligned. Never drop such a control into a raw `
`, and never add a body-level wrapper `
` with a custom `gap-*` that fights `gap-4`. - **Page section rhythm** (integrations/skills/settings): muted `text-small` label + `mt-[9px] mb-3 h-px bg-[var(--border)]` divider, sections stacked `gap-7`. Reuse `SettingsSection` (`app/workspace/[workspaceId]/settings/components/settings-section/settings-section.tsx`) rather than re-deriving it. When a standalone labeled field outside a `ChipModal` needs the same look (e.g. `SkillImport`), match the field rhythm by hand: `flex flex-col gap-[9px]`, muted label, `ChipInput`/`ChipTextarea` control, `text-caption` error below. diff --git a/.cursor/rules/sim-url-state.mdc b/.cursor/rules/sim-url-state.mdc index 1aa10918c8e..cb1d05fa410 100644 --- a/.cursor/rules/sim-url-state.mdc +++ b/.cursor/rules/sim-url-state.mdc @@ -1,6 +1,6 @@ --- description: "Shareable client view-state lives in the URL via nuqs" -globs: ["apps/sim/app/**/*.tsx","apps/sim/app/**/*.ts","apps/sim/app/**/search-params.ts","apps/sim/ee/**/*.tsx","apps/sim/ee/**/*.ts"] +globs: ["apps/sim/app/**/*.tsx","apps/sim/app/workspace/**/*.ts","apps/sim/app/o/**/*.ts","apps/sim/ee/**","apps/sim/hooks/**","apps/sim/stores/**","apps/sim/lib/url-state/**","apps/sim/**/search-params.ts","apps/sim/**/*navigation.ts"] --- diff --git a/CLAUDE.md b/CLAUDE.md index a3f7da54018..c04bd72e209 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -7,7 +7,7 @@ This file (also `AGENTS.md`) holds the repo-wide rules. Area detail lives in `.c - **Package manager**: `bun` and `bunx`, never `npm` and `npx`. - **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 })`. - **Comments**: name things so the code explains itself. TSDoc documents exported APIs and non-obvious modules. An inline `//` is only for a terse, non-obvious *why*, or for a script-enforced `// : ` annotation (`boundary-raw-fetch`, `double-cast-allowed`, `boundary-raw-json`, `untyped-response`, `rq-lint-allow`, `client-boundary-allow`, `utils-lint-allow`, …). History belongs in the commit message. No `====` separators or commented-out code (`check:comment-hygiene` enforces this). The `/you-might-not-need-a-comment` skill applies this to a diff. -- **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. +- **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. For other randomness, `@sim/utils/random` (`randomInt`, `randomFloat`, `randomItem`, `generateRandomBytes`, `generateRandomHex`) — never `Math.random()` or `crypto.randomBytes()`. - **Common utilities**: use the shared helpers from `@sim/utils` instead of inline implementations (`check:utils` bans most of the inline forms below): - `sleep(ms)` from `@sim/utils/helpers` — never `new Promise(resolve => setTimeout(resolve, ms))` - `toError(e)` from `@sim/utils/errors` — normalize caught values to `Error`; never `e instanceof Error ? e : new Error(String(e))` @@ -104,7 +104,7 @@ The `'use client'` server boundary, the app/worker runtime env split, and featur - Tailwind only. Inline `style` only for a genuinely dynamic value or a CSS variable. Never update global styles; keep styling local to the component. `cn()` from `@sim/emcn` for conditional classes. `size-*` for equal height and width (icons default `size-[14px]`), never `h-N w-N`. - Import components, `cn`, and tokens from the `@sim/emcn` barrel; icons from `@sim/emcn/icons`; CSS modules by file path. Never deep-import other component subpaths. -- The chip family is the canonical chrome: `ChipInput`, `ChipTextarea`, `ChipModal`/`ChipModalField`, `ChipSelect`/`ChipCombobox`/`ChipDropdown`, `ChipSwitch`, `ChipDatePicker`, `Chip`/`ChipLink`, `ChipTag`; `DropdownMenu` for context/action menus. Components own their chrome: consumers pass props (`error`, `icon`, `endAdornment`, `inputClassName`) and `className` carries only layout/sizing. Every labeled field inside a `ChipModalBody` is a `ChipModalField`. +- The chip family is the canonical chrome: `ChipInput`, `ChipTextarea`, `ChipModal`/`ChipModalField`, `ChipSelect`/`ChipCombobox`/`ChipDropdown`, `ChipSwitch` (a segmented choice; a boolean toggle stays `Switch`), `ChipDatePicker`, `Chip`/`ChipLink`, `ChipTag`; `DropdownMenu` for context/action menus. Components own their chrome: consumers pass props (`error`, `icon`, `endAdornment`, `inputClassName`) and `className` carries only layout/sizing. Every labeled field inside a `ChipModalBody` is a `ChipModalField`. - Consumer rules, tokens, text scale, and modal rhythm: `.claude/rules/sim-styling.md`. Authoring components in `packages/emcn`: `.claude/rules/emcn-components.md`. Product UI copy: `.claude/rules/sim-ui-copy.md`. Marketing copy and positioning: `.claude/rules/constitution.md`. ## Testing @@ -149,4 +149,4 @@ git fetch origin staging # the block-registry check diffs against it bun run apps/sim/scripts/check-block-registry.ts origin/staging ``` -CI also runs `bun run check:migrations ` (it needs a base ref, so it is not in `check:audits`; run it with `origin/staging` when you touch `packages/db/migrations/**`), checks that `drizzle-kit generate` in `packages/db` produces no new migration, and runs a non-blocking `bun audit`. When an audit fails, its output and its script's header say what the rule protects; fix the code, never the check. Ratchet baselines (`scripts/*baseline.json`) only shrink: regenerate one with the update flag its failure output names (`--update` or `--update-baseline`) after removing violations, never to admit new debt. The one exception is `check:tool-registry-boundary`, whose module-count baseline is re-recorded when growth is deliberate (see its skill). +CI also runs `bun run check:migrations` (it diffs against a base ref, `origin/staging` by default, so it is not in `check:audits`; run it when you touch `packages/db/migrations/**`), checks that `drizzle-kit generate` in `packages/db` produces no new migration, and runs a non-blocking `bun audit`. When an audit fails, its output and its script's header say what the rule protects; fix the code, never the check. Ratchet baselines (`scripts/*baseline.json`) only shrink: regenerate one with `--update` after removing violations; it refuses to admit new debt. The one exception is `check:tool-registry-boundary`, whose module-count baseline is re-recorded with `--update-baseline` when growth is deliberate (see its skill). diff --git a/apps/sim/AGENTS.md b/apps/sim/AGENTS.md index 27a844ddf7e..84feeac85ce 100644 --- a/apps/sim/AGENTS.md +++ b/apps/sim/AGENTS.md @@ -12,6 +12,13 @@ Applies to `apps/sim/**` on top of the root [AGENTS.md](/AGENTS.md), which holds - Tests: `sim-testing.md` and the `test-audit` skill - Landing pages: `app/(landing)/CLAUDE.md`, `landing-seo-geo.md`, `constitution.md` (product language) +For a common task, start from its skill (`.agents/skills//SKILL.md`): + +- New or migrated API route, tool command, or protected operation: `migrate-application-operation`, then `sim-api-contracts.md`; a `/api/v2` route also follows `v2-api-conventions` +- Schema change or migration: `db-migrate` +- Integration (tools, block, trigger): `add-integration`; a knowledge connector: `add-connector` +- Feature flag: `add-feature-flag`; settings page: `add-settings-page`; table column type: `add-column-type`; model: `add-model` + # This is NOT the Next.js you know diff --git a/package.json b/package.json index 4d76f4c6e37..99443cc674e 100644 --- a/package.json +++ b/package.json @@ -59,7 +59,7 @@ "check:sql-date-binding": "bun run scripts/check-sql-date-binding.ts", "check:pending-drop-tables": "bun run scripts/check-pending-drop-tables.ts", "check:zustand-v5": "bun run scripts/check-zustand-v5-selectors.ts", - "check:react-query": "bun run scripts/check-react-query-patterns.ts --check", + "check:react-query": "bun run scripts/check-react-query-patterns.ts", "check:client-boundary": "bun run scripts/check-client-boundary-imports.ts --check", "check:utils": "bun run scripts/check-utils-enforcement.ts", "check:canvas-sentences": "bun run apps/sim/scripts/check-canvas-sentences.ts --require-coverage", diff --git a/packages/testing/src/factories/id.ts b/packages/testing/src/factories/id.ts index 858963f6058..08c95f8c86d 100644 --- a/packages/testing/src/factories/id.ts +++ b/packages/testing/src/factories/id.ts @@ -3,8 +3,7 @@ const URL_SAFE_ALPHABET = 'useandom-26T198340PX75pxJACKVERYMINDBUSHWOLF_GQZbfghj /** * Generates a short, URL-safe random ID for test fixtures. * - * Uses `crypto.getRandomValues()` instead of `crypto.randomUUID()` for - * consistency with the app-level `generateShortId` utility. + * Mirrors the app-level `generateShortId` utility, which is built on `crypto.getRandomValues()`. */ export function shortId(size = 8): string { const bytes = new Uint8Array(size) diff --git a/scripts/check-react-query-patterns.baseline.json b/scripts/check-react-query-patterns.baseline.json deleted file mode 100644 index f0815addbe0..00000000000 --- a/scripts/check-react-query-patterns.baseline.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "generatedFrom": "apps/sim (non-strict zone)", - "counts": {} -} diff --git a/scripts/check-react-query-patterns.ts b/scripts/check-react-query-patterns.ts index 86f8525b6b6..97755a7dc08 100644 --- a/scripts/check-react-query-patterns.ts +++ b/scripts/check-react-query-patterns.ts @@ -21,29 +21,23 @@ * pageParam machinery) are checked, and only when both a queryKey and an * inline-arrow queryFn with a recognizable call are present. * - * Enforcement model (mirrors check-api-validation-contracts.ts): - * - STRICT ZONE (apps/sim/hooks/queries/**): zero tolerance — any violation fails. - * - Elsewhere under apps/sim/**: ratcheted against scripts/check-react-query-patterns.baseline.json - * (fails only when a category's count rises above the recorded baseline). + * Zero tolerance across apps/sim/**: any violation fails. * * Escape hatch: put `// rq-lint-allow: ` on the line directly above the * flagged construct (up to 3 preceding comment lines tolerated). The reason must * be non-empty. * - * Usage: - * bun run scripts/check-react-query-patterns.ts # report - * bun run scripts/check-react-query-patterns.ts --check # CI gate (strict zone + ratchet) - * bun run scripts/check-react-query-patterns.ts --update-baseline + * Usage: bun run scripts/check-react-query-patterns.ts */ -import { readdir, readFile, writeFile } from 'node:fs/promises' +import { readdir, readFile } from 'node:fs/promises' import path from 'node:path' const ROOT = path.resolve(import.meta.dir, '..') const APP_DIR = path.join(ROOT, 'apps/sim') -const BASELINE_PATH = path.join(ROOT, 'scripts/check-react-query-patterns.baseline.json') +/** The shared query-hook layer, where every `*Keys` factory must expose an `all` root key. */ +const QUERY_HOOKS_PREFIX = 'apps/sim/hooks/queries/' const SKIP_DIRS = new Set(['node_modules', '.next', '.turbo', 'coverage', 'dist', 'build']) -const STRICT_PREFIX = 'apps/sim/hooks/queries/' const ALLOW = 'rq-lint-allow:' type Category = @@ -143,7 +137,7 @@ function hasAllow(lines: string[], line: number): boolean { * The optional explicit type argument on a query call — `useQuery({ ... })`. * * Matched rather than ignored because a call carrying one is still a query call: without this - * the scan skipped every generically-typed query, including ten in the strict zone, which then + * the scan skipped every generically-typed query, including ten in `hooks/queries`, which then * reported zero violations while never having looked at them. One level of nesting is enough * for the shapes that occur here (`useQuery>`). */ @@ -429,7 +423,7 @@ function scanFile(rel: string, content: string): Violation[] { } // 4: key factory must have an `all` root (hooks/queries/** only, excluding util key files that compose others) - if (rel.startsWith(STRICT_PREFIX)) { + if (rel.startsWith(QUERY_HOOKS_PREFIX)) { KEYS_FACTORY.lastIndex = 0 let k: RegExpExecArray | null = KEYS_FACTORY.exec(content) for (; k !== null; k = KEYS_FACTORY.exec(content)) { @@ -445,23 +439,7 @@ function scanFile(rel: string, content: string): Violation[] { return violations } -interface Baseline { - generatedFrom: string - counts: Record -} - -async function loadBaseline(): Promise { - try { - return JSON.parse(await readFile(BASELINE_PATH, 'utf8')) - } catch { - return { generatedFrom: 'none', counts: {} } - } -} - async function main() { - const update = process.argv.includes('--update-baseline') - const check = process.argv.includes('--check') - const files = await walk(APP_DIR) const all: Violation[] = [] for (const file of files) { @@ -474,60 +452,16 @@ async function main() { all.push(...scanFile(rel, content)) } - const strict = all.filter((v) => v.file.startsWith(STRICT_PREFIX)) - const ratchet = all.filter((v) => !v.file.startsWith(STRICT_PREFIX)) - - const counts: Record = {} - for (const v of ratchet) counts[v.category] = (counts[v.category] ?? 0) + 1 - - if (update) { - const baseline: Baseline = { generatedFrom: 'apps/sim (non-strict zone)', counts } - await writeFile(BASELINE_PATH, `${JSON.stringify(baseline, null, 2)}\n`) - console.log(`✓ Baseline written: ${JSON.stringify(counts)}`) - process.exit(0) - } - console.log(`React Query pattern audit — scanned ${files.length} files`) - console.log(` strict zone (${STRICT_PREFIX}**) violations: ${strict.length}`) - console.log(` ratchet zone violations: ${ratchet.length} ${JSON.stringify(counts)}`) - - let failed = false - - if (strict.length > 0) { - failed = true - console.error(`\n✗ ${strict.length} violation(s) in the strict zone (${STRICT_PREFIX}**):\n`) - for (const v of strict) { + if (all.length > 0) { + console.error(`\n✗ ${all.length} violation(s):\n`) + for (const v of all) { console.error(` ${v.file}:${v.line} [${v.category}]`) console.error(` ${v.snippet}`) console.error(` → ${v.message}\n`) } + process.exit(1) } - - if (!check && ratchet.length > 0) { - console.error(`\nRatchet-zone occurrences (not failing without --check):`) - for (const v of ratchet) { - console.error(` ${v.file}:${v.line} [${v.category}] ${v.snippet}`) - } - } - - if (check) { - const baseline = await loadBaseline() - for (const [category, count] of Object.entries(counts)) { - const base = baseline.counts[category] ?? 0 - if (count > base) { - failed = true - console.error( - `\n✗ ratchet regression: ${category} rose to ${count} (baseline ${base}). ` + - `Fix the new occurrence(s) or annotate with // ${ALLOW} .` - ) - for (const v of ratchet.filter((x) => x.category === category)) { - console.error(` ${v.file}:${v.line} ${v.snippet}`) - } - } - } - } - - if (failed) process.exit(1) console.log('\n✓ React Query pattern audit passed.') process.exit(0) } diff --git a/scripts/check-test-patterns.ts b/scripts/check-test-patterns.ts index 1d810e6e663..afaa9dd82b7 100644 --- a/scripts/check-test-patterns.ts +++ b/scripts/check-test-patterns.ts @@ -19,7 +19,7 @@ * Run: `bun run check:test-patterns` */ import { execFileSync } from 'node:child_process' -import { readdirSync, readFileSync, writeFileSync } from 'node:fs' +import { existsSync, readdirSync, readFileSync, writeFileSync } from 'node:fs' import path from 'node:path' import { parse } from '@babel/parser' @@ -285,14 +285,35 @@ function key(violation: Violation): string { const violations = collect() const current = [...new Set(violations.map(key))].sort() -if (process.argv.includes('--update')) { - writeFileSync(BASELINE, `${JSON.stringify(current, null, 2)}\n`) - console.log(`Wrote ${current.length} baseline entries to ${path.relative(ROOT, BASELINE)}`) - process.exit(0) +/** + * The committed baseline. A missing file fails closed: restore it from git. `--update --init` + * is the only way to create one, and it accepts every current violation. + */ +function readBaseline(): string[] { + if (existsSync(BASELINE)) return JSON.parse(readFileSync(BASELINE, 'utf8')) + if (process.argv.includes('--update') && process.argv.includes('--init')) return current + console.error( + `✗ ${path.relative(ROOT, BASELINE)} is missing. Restore it from git; ` + + 'create a new one only with --update --init.' + ) + process.exit(1) } -const baseline = new Set(JSON.parse(readFileSync(BASELINE, 'utf8'))) +const baseline = new Set(readBaseline()) const added = current.filter((entry) => !baseline.has(entry)) + +if (process.argv.includes('--update')) { + // Shrink-only: drop fixed entries, never admit a new one, and write nothing if refusing. + for (const entry of added) { + const [rule, file, detail] = entry.split('\t') + console.error(`✗ not baselined — fix it: ${rule}: ${file} (${detail})`) + } + if (added.length) process.exit(1) + const kept = current.filter((entry) => baseline.has(entry)) + writeFileSync(BASELINE, `${JSON.stringify(kept, null, 2)}\n`) + console.log(`Wrote ${kept.length} baseline entries to ${path.relative(ROOT, BASELINE)}`) + process.exit(0) +} const currentSet = new Set(current) const stale = [...baseline].filter((entry) => !currentSet.has(entry)) diff --git a/scripts/check-utils-enforcement.ts b/scripts/check-utils-enforcement.ts index 8facefb0075..63da16ffbb7 100644 --- a/scripts/check-utils-enforcement.ts +++ b/scripts/check-utils-enforcement.ts @@ -46,8 +46,6 @@ const ALLOWLISTED_FILES = new Set([ 'apps/sim/lib/execution/isolated-vm-worker.cjs', // Emits the sandbox-side event filter as plain JS source, which cannot import @sim/utils 'apps/sim/executor/handlers/pi/cloud/event-filter-source.ts', - // Uses crypto.getRandomValues() directly (not crypto.randomUUID) — TSDoc comment triggers false positive - 'packages/testing/src/factories/id.ts', ]) /** `s.slice(0, n)` plus a suffix, by `+` or in a template literal; `\1` is `s` and `\2` is `n`. */ diff --git a/scripts/run-audits.ts b/scripts/run-audits.ts index 1c44bddc26f..b06890355cd 100644 --- a/scripts/run-audits.ts +++ b/scripts/run-audits.ts @@ -16,7 +16,7 @@ import path from 'node:path' /** `check:*` scripts this runner deliberately does not own, and why. */ const EXCLUDED: Record = { 'check:audits': 'this runner', - 'check:migrations': 'needs a git base ref argument', + 'check:migrations': 'diffs against a git base ref (origin/staging by default)', 'check:api-validation': 'superseded by the :strict variant, which this runner does run', 'check:dead-code': 'check:unused-exports gates the same issues in its single knip pass', }