From 378298f2bd554bf30425af548dcf7f7d0e04d3b8 Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Fri, 6 Nov 2026 11:32:17 -0800 Subject: [PATCH 1/3] chore(agents): trim root CLAUDE.md by ablation; AGENTS.md links to it --- .agents/skills/emil-design-eng/SKILL.md | 2 +- .../make-interfaces-feel-better/SKILL.md | 2 +- .claude/rules/sim-styling.md | 2 + .cursor/rules/sim-styling.mdc | 2 + AGENTS.md | 165 +----------------- CLAUDE.md | 150 ++++------------ 6 files changed, 44 insertions(+), 279 deletions(-) mode change 100644 => 120000 AGENTS.md diff --git a/.agents/skills/emil-design-eng/SKILL.md b/.agents/skills/emil-design-eng/SKILL.md index 9ac52fe59da..6f9edf24c4d 100644 --- a/.agents/skills/emil-design-eng/SKILL.md +++ b/.agents/skills/emil-design-eng/SKILL.md @@ -6,7 +6,7 @@ description: This skill encodes Emil Kowalski's philosophy on UI polish, compone # Design Engineering -> **In this repo:** emcn owns component chrome (press states, tooltip/popover/menu animation, easing): apply these principles by proposing changes in `packages/emcn`, not per call site, and add no global CSS variables or keyframes (CLAUDE.md "Styling and EMCN"). Animation uses `import { motion } from 'framer-motion'`. Gate touch-unsafe hover with the `hover-hover:` Tailwind variant (plain `hover:` is intentionally ungated). `.claude/rules/sim-styling.md` and `.claude/rules/emcn-components.md` win on any conflict. +> **In this repo:** emcn owns component chrome (press states, tooltip/popover/menu animation, easing): apply these principles by proposing changes in `packages/emcn`, not per call site, and add no global CSS variables or keyframes (`.claude/rules/sim-styling.md`). Animation uses `import { motion } from 'framer-motion'`. Gate touch-unsafe hover with the `hover-hover:` Tailwind variant (plain `hover:` is intentionally ungated). `.claude/rules/sim-styling.md` and `.claude/rules/emcn-components.md` win on any conflict. You are a design engineer with the craft sensibility. You build interfaces where every detail compounds into something that feels right. You understand that in a world where everyone's software is good enough, taste is the differentiator. diff --git a/.agents/skills/make-interfaces-feel-better/SKILL.md b/.agents/skills/make-interfaces-feel-better/SKILL.md index 4e62a1392a8..dfb709f427a 100644 --- a/.agents/skills/make-interfaces-feel-better/SKILL.md +++ b/.agents/skills/make-interfaces-feel-better/SKILL.md @@ -49,7 +49,7 @@ Animate contextual icons with opacity, scale, and blur instead of toggling visib ### 8. Font Smoothing -Apply `-webkit-font-smoothing: antialiased` to the root layout on macOS for crisper text. In this repo, do not add root-level smoothing or other global styles (CLAUDE.md "Styling and EMCN"); raise it as a design-system proposal instead. +Apply `-webkit-font-smoothing: antialiased` to the root layout on macOS for crisper text. In this repo, do not add root-level smoothing or other global styles (`.claude/rules/sim-styling.md`); raise it as a design-system proposal instead. ### 9. Tabular Numbers diff --git a/.claude/rules/sim-styling.md b/.claude/rules/sim-styling.md index 300c63f9260..17a563e4b0b 100644 --- a/.claude/rules/sim-styling.md +++ b/.claude/rules/sim-styling.md @@ -89,6 +89,8 @@ Draw a line with a real `border-*` utility. Never hand-roll one as `shadow-[inse ## Chip Components (consumer usage) +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. Import components, `cn`, and tokens from the `@sim/emcn` barrel and icons from `@sim/emcn/icons`; never deep-import other component subpaths. Never update global styles; keep styling local to the component. + `ChipInput`, `ChipTextarea`, `ChipModal*` own their full chrome. Consumers describe intent through PROPS; they never re-style the chrome. The canonical chrome lives in `packages/emcn/src/components/chip/chrome.ts` (all tokens are re-exported from the `@sim/emcn` barrel — no subpath import needed) — never hand-roll `rounded-lg`/`border`/`bg-[var(--surface-5)]`/`h-[30px]`/`px-2`/`text-sm`/focus rings. ### Props over className diff --git a/.cursor/rules/sim-styling.mdc b/.cursor/rules/sim-styling.mdc index 3f00af5afff..17a82739a0f 100644 --- a/.cursor/rules/sim-styling.mdc +++ b/.cursor/rules/sim-styling.mdc @@ -89,6 +89,8 @@ Draw a line with a real `border-*` utility. Never hand-roll one as `shadow-[inse ## Chip Components (consumer usage) +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. Import components, `cn`, and tokens from the `@sim/emcn` barrel and icons from `@sim/emcn/icons`; never deep-import other component subpaths. Never update global styles; keep styling local to the component. + `ChipInput`, `ChipTextarea`, `ChipModal*` own their full chrome. Consumers describe intent through PROPS; they never re-style the chrome. The canonical chrome lives in `packages/emcn/src/components/chip/chrome.ts` (all tokens are re-exported from the `@sim/emcn` barrel — no subpath import needed) — never hand-roll `rounded-lg`/`border`/`bg-[var(--surface-5)]`/`h-[30px]`/`px-2`/`text-sm`/focus rings. ### Props over className diff --git a/AGENTS.md b/AGENTS.md deleted file mode 100644 index c0893c0170d..00000000000 --- a/AGENTS.md +++ /dev/null @@ -1,164 +0,0 @@ -# Sim Development Guidelines - -This file (also `AGENTS.md`) holds the repo-wide rules. Area detail lives in `.claude/rules/*.md` (indexed in `apps/sim/AGENTS.md`): Claude loads each one by path, and any other agent reads the file a section points to before editing in that area. Skills live in `.agents/skills/`. - -## Global Standards - -- **Package manager**: `bun` and `bunx`, never `npm` and `npx`. -- **Logging**: `createLogger` from `@sim/logger`; `logger.info` / `logger.warn` / `logger.error`, never `console.log`. Messages are static text; ids go in metadata (`logger.info('Sync started', { requestId, documentId })`), never a `[${requestId}]` tag or an interpolated request id, so lines group and filter by field (`check:source-conventions` rule `log-message-tag`). Inside `withRouteHandler` every line already carries the request ID, so a `generateRequestId()` id there needs no field and 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, commented-out code, or ESLint directives (`eslint-disable`/`eslint-enable`; nothing runs ESLint; `check:comment-hygiene` enforces all three). 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. For other randomness, `@sim/utils/random` (`randomInt`, `randomFloat`, `randomItem`, `generateRandomBytes`, `generateRandomHex`) — never `Math.random()` or `crypto.randomBytes()`. -- **Common utilities**: use the shared helpers from the `@sim/utils/` subpaths 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))` - - `getErrorMessage(e, fallback?)` from `@sim/utils/errors` — never `e instanceof Error ? e.message : 'fallback'` - - `structuredClone(value)` — built-in deep clone; never `JSON.parse(JSON.stringify(...))` - - `omit(obj, keys)` / `filterUndefined(obj)` from `@sim/utils/object` — never `Object.fromEntries(Object.entries(...).filter(...))` - - `isRecordLike(value)` from `@sim/utils/object` — never redeclare `typeof value === 'object' && value !== null && !Array.isArray(value)` - - `toRecord(value)` / `toRecordOrNull(value)` / `toArray(value)` from `@sim/utils/object` — coerce an untyped payload value; never inline `isRecordLike(v) ? v : {}` or `Array.isArray(v) ? v : []`. Where the source is already typed, keep the inline `Array.isArray` check: it narrows, while `toArray` asserts - - `toStringOrNull(value)` / `toNumberOrNull(value)` / `toBooleanOrNull(value)` from `@sim/utils/coerce` — read one scalar out of an untyped payload; never declare a local one-liner byte-identical to one of these. Keep a local helper that differs: `undefined` instead of `null` changes the wire shape, and a `Number.isFinite` or string-parse variant is a stricter check these omit - - `truncate(str, maxLength, suffix?)` from `@sim/utils/string` — never inline slice + ellipsis - - `escapeRegExp(value)` from `@sim/utils/string` — never inline `replace(/[.*+?^${}()|[\]\\]/g, '\\$&')` - - `compareStrings(left, right)` from `@sim/utils/string` — code-unit ordering for hashes, fingerprints, and cross-process comparisons; never `localeCompare` there - - `backoffWithJitter(attempt, retryAfterMs, options?)` / `parseRetryAfter(header)` from `@sim/utils/retry` — never reimplement exponential backoff inline -- **Deployment flags in the browser**: client code inside a workspace, organization, or standalone settings surface reads `hosted`, `billingEnabled`, `chatEnabled`, and the enterprise feature set through `useDeploymentShape()` (components) or `getDeploymentShape()` (block conditions, stores, helpers) from `@/lib/core/config/deployment-shape`, never `isHosted`/`isBillingEnabled`/... from `env-flags`. Those constants freeze at module init from the root layout's `NEXT_PUBLIC_*` transport, which Next's bare 404 shell and `global-error` never emit, so a recovered tab would render Sim Cloud as self-hosted; the reader is seeded from the server-resolved workspace host context, organization layout, or standalone settings layout instead. Server code keeps reading `env-flags`. -- **Type-checking**: `bun run type-check` at the root checks every workspace (`bun run type-check` inside a workspace checks just that one). `apps/sim`'s tsconfig excludes `*.test.ts(x)`, so `type-check` skips its tests; CI's `test-types` job ratchets their errors per file and TS code instead (`check:test-types`, whose header says how to fix a finding): a test file may not gain one. Never remove the `@typescript/native` alias from the root `devDependencies`: nothing imports it, but without it a bare `tsc` resolves to the ~10x slower TypeScript 6 compiler. `bun run check:native-typecheck` enforces this; its header explains the bin-resolution trap. - -## Architecture - -### Repository layout - -``` -apps/ -├── sim/ # Next.js app: UI, API routes, workflow builder, executor -│ ├── app/ # App router — pages and API routes (app/api/**) -│ ├── blocks/ # Block definitions and registry -│ ├── tools/ # Tool definitions and registry -│ ├── triggers/ # Trigger definitions and registry -│ ├── connectors/ # Knowledge base connectors -│ ├── executor/ # Workflow execution engine -│ ├── providers/ # LLM provider integrations -│ ├── components/ # Shared app UI (ui/, icons, …) -│ ├── hooks/ # Shared hooks (queries/, selectors/) -│ ├── stores/ # Zustand stores -│ ├── lib/ # App-wide modules, incl. lib/api/contracts and lib//application -│ └── ee/ # Enterprise features -├── realtime/ # Bun Socket.IO server (collaborative workflow builder) -├── desktop/ # Electron shell around the hosted web app -├── docs/ # Documentation site -└── pii/ # Python PII detection service (not in the JS/turbo build) - -packages/ -├── emcn/ # @sim/emcn — design system (chip family, tokens, icons) -├── db/ # @sim/db — Drizzle schema, migrations, client -├── auth/ # @sim/auth — shared Better Auth verifier -├── platform-authz/ # @sim/platform-authz — workspace + workflow authz (subpath exports) -├── audit/ logger/ security/ utils/ runtime-secrets/ deployment-config/ -├── realtime-protocol/ browser-protocol/ terminal-protocol/ desktop-bridge/ -├── workflow-types/ workflow-persistence/ workflow-renderer/ -├── testing/ # @sim/testing — test factories and mocks -├── tsconfig/ # shared tsconfig presets -└── cli/ sim-cli/ sim-setup/ ts-sdk/ python-sdk/ # published CLIs and SDKs -``` - -### Package boundaries - -- `apps/* → packages/*` only. Packages never import from `apps/*`. -- `apps/realtime` avoids Next.js, React, the block/tool registry, provider SDKs, and the executor. Never add imports from `@/lib/webhooks/providers/*`, `@/executor/*`, `@/blocks/*`, or `@/tools/*` to any package it consumes; it calls back into `apps/sim` only over internal HTTP with `INTERNAL_API_SECRET`. CI enforces this via `scripts/check-monorepo-boundaries.ts` and `scripts/check-realtime-prune-graph.ts`. -- Auth is shared across both apps via the Better Auth "Shared Database Session" pattern (same `BETTER_AUTH_SECRET`, same DB via `@sim/db`). - -### Application Operation Boundary - -- Every protected read, write, canonical resource lookup, or authorization-sensitive reference resolution enters through an authorized application use case. -- Define one stable semantic operation with its minimum role, workspace-key policy, allowed principal kinds, and delegated services. Internal APIs, v2 APIs, Copilot, and trusted tools call the same use case when the domain behavior is the same. -- Surface adapters authenticate and construct a `Principal`, apply request-rate policy, parse contracts, map input, and present their own result. They never query protected data, decide resource authorization, implement business transactions, or record semantic audit (`check:source-conventions` ratchets value imports of `@sim/db` under `app/api/`). -- Application use cases load canonical context, compare asserted scope, authorize current access, execute managers/repositories, project semantic audit, and trigger shared domain effects. Managers accept canonical IDs and scope, never credentials or principals. Application code stays surface-neutral (`check:boundaries`): never `next/server` or `app/api/**`, and never a runtime import of a route contract object, presenter, or Copilot handler (contract types, schemas, and constants are fine); a surface fact, such as a cursor's route, comes in as input. -- Copilot is a surface adapter. Use `createCopilotApplicationAdapter` and the domain's registered operation object; never a Copilot-only authorization or business implementation. -- Protected compound mutations belong in one top-level semantic application operation, never a sequence of independently committing mutations in a route or tool adapter. -- Never substitute a billing owner, uploader, creator, or API-key owner for the acting principal. Fail fast when the identity model or operation policy cannot express the caller. -- Use the `migrate-application-operation` skill whenever creating or migrating a protected endpoint, tool command, or resource method. - -The `'use client'` server boundary, the app/worker runtime env split, and feature folder layout are in `.claude/rules/sim-architecture.md`. - -## Code Conventions - -- **Naming**: components PascalCase (`WorkflowList`); hooks `use*`; files kebab-case (`workflow-list.tsx`); constants SCREAMING_SNAKE_CASE; interfaces PascalCase with a suffix (`WorkflowListProps`); stores `stores//store.ts`. Inside `lib/`, `executor/`, `providers/`, `stores/`, `hooks/`, `serializer/`, and a package's `src/`, a file never starts with its folder's name (`logs/views.ts`, not `logs/log-views.ts`), and a file in `utils/` or `helpers/` never repeats that role (`date.ts`, not `date-utils.ts`); `feature/feature.tsx` stays the component convention. `check:file-names` ratchets this. -- **Imports**: absolute (`@/...`) only, never relative (a barrel `index.ts` re-exports its own siblings relatively). `check:source-conventions` ratchets relative imports in `apps/sim`. 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`. -- **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(null)`). -- **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, or a type parameter kept for API shape. `const { a, ...rest } = obj` to omit keys is allowed. The rules carry no autofix, so `bun run lint` will not rename anything for you. -- **Components**: `'use client'` only for hooks or browser APIs (`check:client-boundary` guards the server boundary). 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. -- **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`. -- **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. -- **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`. -- **Caching**: `lru-cache` with a `max` ceiling, never a hand-rolled TTL `Map`; a lifecycle map is not a cache; cache the gate, never the credential: `.claude/rules/sim-caching.md`. - -## API Contracts and Routes - -- Request/response shapes for every route under `apps/sim/app/api/**` live in `apps/sim/lib/api/contracts/**`, built with `defineRouteContract`. Export the contract; export a named schema or type alias only when another module imports it (`check:unused-exports`). Routes never import `zod` or define route-local boundary schemas; clients never write ad-hoc wire types or `z.input`/`z.output`. -- Every route handler runs inside `withRouteHandler`. Ordinary internal and v2 routes use the shared builders (`defineInternalJsonRoute`, `defineV2JsonRoute`, binary/stream variants), which already apply it — never double-wrap. Raw `withRouteHandler` is only for documented protocol or lifecycle exceptions. Never export a bare `async function GET/POST/...` (`check:source-conventions`). -- Same-origin JSON calls go through `requestJson(contract, ...)` from `@/lib/api/client/request`. A raw `fetch` is only for streaming, binary downloads, multipart uploads, signed URLs, OAuth redirects, or external origins, and carries `// boundary-raw-fetch: `. -- The other script-enforced exceptions are `// double-cast-allowed:`, `// boundary-raw-json:`, and `// untyped-response:`. Never add one to silence a fixable finding. -- `bun run check:api-validation:strict` must pass. Full contract rules, route pattern, annotation placement, the end-to-end order, and the schema review checklist: `.claude/rules/sim-api-contracts.md`. React Query hooks (key factories, named `staleTime` constants, `signal`, invalidation, server prefetch): `.claude/rules/sim-queries.md`. - -## Styling and EMCN - -- Tailwind only. Inline `style` only for a genuinely dynamic value or a CSS variable; type size is a named token, never `text-[Npx]` (`check:source-conventions` ratchets both). 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` (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 - -Most unit tests in a codebase like this restate the code they test. They pass on the first run, break on every refactor, and catch nothing that type-check, `next build`, `bun run check:audits`, or a real end-to-end run would miss. Test for confidence, not coverage. - -- **Never write unit tests after you write code.** A test written to describe code that already exists restates the implementation and proves nothing. If the change needs proof, prove it end to end. -- **Highly prefer E2E tests.** Use them to verify complex features work, against the real boundary: real Postgres/Redis (`*.integration.ts`), the running app over real HTTP (`apps/sim/scripts/test-*-e2e.ts`), or the packaged desktop app (`apps/desktop/e2e`, Playwright). At the end of an E2E test, produce a verifiable and repeatable artifact — a JSON report of each check with status and duration, an HTTP status log, a trace, or a screenshot — written to a caller-supplied `_REPORT_PATH` and uploaded by CI on failure. `apps/sim/scripts/test-scim-e2e.ts` is the reference. -- **If you must test a system in isolation, first write down all the ways it could fail, then write the code.** Each failure mode (bad input, boundary, concurrency, partial failure, permission denial, resource cap) becomes one test that fails before the code exists. -- A regression test must fail on the pre-fix code. Revert each guard of the fix and watch its test go red before you trust it. -- Never write tests that restate declarations (block/tool/provider config, registries, constants, schemas accepting valid input), mock every collaborator and assert `toHaveBeenCalledWith` on the mocks, check rendered text or class names, or test mocks and factories themselves. -- Never hand-roll a mock or test helper that `apps/sim/vitest.setup.ts` or `@sim/testing` already provides; a module mocked in a third file gets one central mock. `bun run check:test-patterns` enforces this. - -Use the `test-audit` skill whenever you write, change, review, or sweep tests — it holds the authoring gate, the junk patterns, and the retention bar. Test layers, file naming, and Vitest mechanics (global mocks, `@sim/testing`, performance rules) are in `.claude/rules/sim-testing.md`. - -## Integrations - -Build order: **Tools** → **Block** → **Icon** → optional **Trigger**, starting from the service's API docs. Use the skills: `/add-integration` (end-to-end), `/add-tools`, `/add-block`, `/add-trigger`. Two rules the skills assume: - -- **Tool IDs are `snake_case`** (`service_action`), registered in `tools/registry.ts`; blocks register in `blocks/registry-maps.ts` (`BLOCK_REGISTRY` + `BLOCK_META_REGISTRY`, alphabetically). -- **Type coercions go in `tools.config.params`** (runs at execution, after variable resolution), never in `tools.config.tool` (runs at serialization, where `Number()` destroys dynamic `` references). - -A surface that lists integrations to a viewer (a catalog, picker, or `@` menu) reads `useIntegrationAccess` (`hooks/use-integration-access.ts`): the one verdict across permission group, deployment allowlist, and deployment OAuth configuration, which fails closed until the viewer's policy loads. Never map over a raw catalog (`INTEGRATIONS`, `listIntegrations()`, `INTEGRATION_METADATA`) there; `check:integration-access` fails the build. - -Remaining block/tool/trigger rules: `.claude/rules/sim-integrations.md`. Canvas sentences: `apps/sim/blocks/AGENTS.md`. - -## Tables - -Table column types are registry entries in `apps/sim/lib/table/column-types/` — one file per type owning its label, icon, storage cast, coercion, validation, conversion compatibility, formatting, and editor. `Record` on `registry.ts` and `registry.server.ts` is a compile-time completeness gate: adding a type to the union errors until both entries exist. - -Never add a `case 'sometype':` outside `column-types/`, except the one documented import-path switch (`coerceValue` in `lib/table/import.ts`), which every new type extends — a missing arm fails silently (a wrong `jsonbCast` breaks every filter on the column). If a consumer needs per-type knowledge, add a registry field. Use `/add-column-type` for the full procedure. - -## How your work is checked - -Before declaring a change done, run the local gate from the repo root; CI runs these and fails on any of them: - -```bash -bun run lint # biome format + lint, autofixes (CI runs lint:check) -bun run type-check # every workspace -bun run check:audits # every check:* audit, generated-artifact checks included -bun run test # script tests, then every workspace's Vitest suite -git fetch origin staging # the block-registry check diffs against it -bun run apps/sim/scripts/check-block-registry.ts origin/staging -``` - -Author a `packages/db` schema change or migration with the `db-migrate` 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). - - - -# This is NOT the Turborepo you know - -Turborepo configuration, task behavior, and CLI commands can vary between installed versions and may differ from your training data. Resolve the `turbo` package from this file's directory or relevant workspace; in monorepos, it may not be visible from the repository root. For example, run `node -p "require.resolve('turbo/package.json')"` from a workspace that depends on `turbo`. - -Read `docs/README.md` inside that installed package first, then read the relevant pages from its `docs/` directory before changing Turborepo configuration or commands. Heed deprecation notices. These bundled docs match the installed package version and are available without network access. - -This block is written and re-added by `turbo` before repository-scoped commands when an AI agent is detected. In the Turborepo source repository, its template is defined in `crates/turborepo-cli/src/cli/agent_guidance.rs`. Removing the managed block while updates are enabled means a later qualifying invocation will add it again. Set `"agentGuidance": false` in the root `turbo.json` or `turbo.jsonc` to opt out; this does not remove an existing block. Keep the block committed with your work to avoid an uncommitted change on the next agent invocation. - diff --git a/AGENTS.md b/AGENTS.md new file mode 120000 index 00000000000..681311eb9cf --- /dev/null +++ b/AGENTS.md @@ -0,0 +1 @@ +CLAUDE.md \ No newline at end of file diff --git a/CLAUDE.md b/CLAUDE.md index 6162cd1934b..9d120e53fc7 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,145 +1,69 @@ # Sim Development Guidelines -This file (also `AGENTS.md`) holds the repo-wide rules. Area detail lives in `.claude/rules/*.md` (indexed in `apps/sim/AGENTS.md`): Claude loads each one by path, and any other agent reads the file a section points to before editing in that area. Skills live in `.agents/skills/`. +This file (also `AGENTS.md`) holds the repo-wide rules: Global Standards, Architecture (incl. the Application Operation Boundary), Code Conventions, API Contracts and Routes, Testing, Integrations, and How your work is checked (the local gate to run before calling a change done). Area detail lives in `.claude/rules/*.md`, indexed in `apps/sim/AGENTS.md`: Claude loads each one by path, and any other agent reads the file a section points to before editing in that area. Skills live in `.agents/skills/`. + +Most rules here are enforced by a `check:*` audit or biome. When one fails, its message and its script's header say what the rule protects and how to fix it: fix the code, never the check, and never add an annotation (`// boundary-raw-fetch:`, `// double-cast-allowed:`, …) to silence a fixable finding. ## Global Standards - **Package manager**: `bun` and `bunx`, never `npm` and `npx`. -- **Logging**: `createLogger` from `@sim/logger`; `logger.info` / `logger.warn` / `logger.error`, never `console.log`. Messages are static text; ids go in metadata (`logger.info('Sync started', { requestId, documentId })`), never a `[${requestId}]` tag or an interpolated request id, so lines group and filter by field (`check:source-conventions` rule `log-message-tag`). Inside `withRouteHandler` every line already carries the request ID, so a `generateRequestId()` id there needs no field and 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, commented-out code, or ESLint directives (`eslint-disable`/`eslint-enable`; nothing runs ESLint; `check:comment-hygiene` enforces all three). 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. For other randomness, `@sim/utils/random` (`randomInt`, `randomFloat`, `randomItem`, `generateRandomBytes`, `generateRandomHex`) — never `Math.random()` or `crypto.randomBytes()`. -- **Common utilities**: use the shared helpers from the `@sim/utils/` subpaths 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))` - - `getErrorMessage(e, fallback?)` from `@sim/utils/errors` — never `e instanceof Error ? e.message : 'fallback'` - - `structuredClone(value)` — built-in deep clone; never `JSON.parse(JSON.stringify(...))` - - `omit(obj, keys)` / `filterUndefined(obj)` from `@sim/utils/object` — never `Object.fromEntries(Object.entries(...).filter(...))` - - `isRecordLike(value)` from `@sim/utils/object` — never redeclare `typeof value === 'object' && value !== null && !Array.isArray(value)` - - `toRecord(value)` / `toRecordOrNull(value)` / `toArray(value)` from `@sim/utils/object` — coerce an untyped payload value; never inline `isRecordLike(v) ? v : {}` or `Array.isArray(v) ? v : []`. Where the source is already typed, keep the inline `Array.isArray` check: it narrows, while `toArray` asserts - - `toStringOrNull(value)` / `toNumberOrNull(value)` / `toBooleanOrNull(value)` from `@sim/utils/coerce` — read one scalar out of an untyped payload; never declare a local one-liner byte-identical to one of these. Keep a local helper that differs: `undefined` instead of `null` changes the wire shape, and a `Number.isFinite` or string-parse variant is a stricter check these omit - - `truncate(str, maxLength, suffix?)` from `@sim/utils/string` — never inline slice + ellipsis - - `escapeRegExp(value)` from `@sim/utils/string` — never inline `replace(/[.*+?^${}()|[\]\\]/g, '\\$&')` - - `compareStrings(left, right)` from `@sim/utils/string` — code-unit ordering for hashes, fingerprints, and cross-process comparisons; never `localeCompare` there - - `backoffWithJitter(attempt, retryAfterMs, options?)` / `parseRetryAfter(header)` from `@sim/utils/retry` — never reimplement exponential backoff inline -- **Deployment flags in the browser**: client code inside a workspace, organization, or standalone settings surface reads `hosted`, `billingEnabled`, `chatEnabled`, and the enterprise feature set through `useDeploymentShape()` (components) or `getDeploymentShape()` (block conditions, stores, helpers) from `@/lib/core/config/deployment-shape`, never `isHosted`/`isBillingEnabled`/... from `env-flags`. Those constants freeze at module init from the root layout's `NEXT_PUBLIC_*` transport, which Next's bare 404 shell and `global-error` never emit, so a recovered tab would render Sim Cloud as self-hosted; the reader is seeded from the server-resolved workspace host context, organization layout, or standalone settings layout instead. Server code keeps reading `env-flags`. -- **Type-checking**: `bun run type-check` at the root checks every workspace (`bun run type-check` inside a workspace checks just that one). `apps/sim`'s tsconfig excludes `*.test.ts(x)`, so `type-check` skips its tests; CI's `test-types` job ratchets their errors per file and TS code instead (`check:test-types`, whose header says how to fix a finding): a test file may not gain one. Never remove the `@typescript/native` alias from the root `devDependencies`: nothing imports it, but without it a bare `tsc` resolves to the ~10x slower TypeScript 6 compiler. `bun run check:native-typecheck` enforces this; its header explains the bin-resolution trap. +- **Logging**: `createLogger` from `@sim/logger`, never `console.log`. Messages are static text and ids go in metadata (`logger.info('Sync started', { documentId })`); inside `withRouteHandler` every line already carries the request ID. +- **Comments**: name things so the code explains itself. TSDoc on exported APIs and non-obvious modules; an inline `//` only for a terse, non-obvious *why* or a script-enforced `// : ` annotation. History goes in the commit message. No `====` separators, commented-out code, or ESLint directives (`check:comment-hygiene`). +- **Common utilities**: use the `@sim/utils/` helpers (`id`, `random`, `errors`, `object`, `coerce`, `string`, `helpers`, `retry`) over inline versions; `check:utils` bans the inline forms and names the replacement. It cannot catch these, so reach for them yourself: `toArray` and `toStringOrNull`/`toNumberOrNull`/`toBooleanOrNull` for untyped payloads, `compareStrings` (never `localeCompare`) for hashes and cross-process ordering, and `backoffWithJitter`/`parseRetryAfter` for retries. +- **Deployment flags in the browser**: client code reads `hosted`, `billingEnabled`, `chatEnabled`, and enterprise flags through `useDeploymentShape()` / `getDeploymentShape()` from `@/lib/core/config/deployment-shape`, never `env-flags` (its values freeze wrong on 404 and error shells). Server code keeps `env-flags` (`check:client-boundary`). +- **Type-checking**: `bun run type-check` at the root checks every workspace. `apps/sim` excludes `*.test.ts(x)` from it; CI's `check:test-types` ratchets their errors per file instead. Keep the unused `@typescript/native` root devDependency (`check:native-typecheck`). ## Architecture -### Repository layout - -``` -apps/ -├── sim/ # Next.js app: UI, API routes, workflow builder, executor -│ ├── app/ # App router — pages and API routes (app/api/**) -│ ├── blocks/ # Block definitions and registry -│ ├── tools/ # Tool definitions and registry -│ ├── triggers/ # Trigger definitions and registry -│ ├── connectors/ # Knowledge base connectors -│ ├── executor/ # Workflow execution engine -│ ├── providers/ # LLM provider integrations -│ ├── components/ # Shared app UI (ui/, icons, …) -│ ├── hooks/ # Shared hooks (queries/, selectors/) -│ ├── stores/ # Zustand stores -│ ├── lib/ # App-wide modules, incl. lib/api/contracts and lib//application -│ └── ee/ # Enterprise features -├── realtime/ # Bun Socket.IO server (collaborative workflow builder) -├── desktop/ # Electron shell around the hosted web app -├── docs/ # Documentation site -└── pii/ # Python PII detection service (not in the JS/turbo build) - -packages/ -├── emcn/ # @sim/emcn — design system (chip family, tokens, icons) -├── db/ # @sim/db — Drizzle schema, migrations, client -├── auth/ # @sim/auth — shared Better Auth verifier -├── platform-authz/ # @sim/platform-authz — workspace + workflow authz (subpath exports) -├── audit/ logger/ security/ utils/ runtime-secrets/ deployment-config/ -├── realtime-protocol/ browser-protocol/ terminal-protocol/ desktop-bridge/ -├── workflow-types/ workflow-persistence/ workflow-renderer/ -├── testing/ # @sim/testing — test factories and mocks -├── tsconfig/ # shared tsconfig presets -└── cli/ sim-cli/ sim-setup/ ts-sdk/ python-sdk/ # published CLIs and SDKs -``` - -### Package boundaries - -- `apps/* → packages/*` only. Packages never import from `apps/*`. -- `apps/realtime` avoids Next.js, React, the block/tool registry, provider SDKs, and the executor. Never add imports from `@/lib/webhooks/providers/*`, `@/executor/*`, `@/blocks/*`, or `@/tools/*` to any package it consumes; it calls back into `apps/sim` only over internal HTTP with `INTERNAL_API_SECRET`. CI enforces this via `scripts/check-monorepo-boundaries.ts` and `scripts/check-realtime-prune-graph.ts`. -- Auth is shared across both apps via the Better Auth "Shared Database Session" pattern (same `BETTER_AUTH_SECRET`, same DB via `@sim/db`). +- **Package boundaries**: `apps/* → packages/*` only; packages never import from `apps/*`. `apps/realtime` (a Bun Socket.IO server) avoids Next.js, React, the block/tool registry, provider SDKs, and the executor, and calls `apps/sim` only over internal HTTP with `INTERNAL_API_SECRET` (`check:boundaries`, `check:realtime-prune`). Both apps share Better Auth sessions through `@sim/db`. +- `'use client'` boundary, app/worker env split, and feature folder layout: `.claude/rules/sim-architecture.md`. ### Application Operation Boundary -- Every protected read, write, canonical resource lookup, or authorization-sensitive reference resolution enters through an authorized application use case. -- Define one stable semantic operation with its minimum role, workspace-key policy, allowed principal kinds, and delegated services. Internal APIs, v2 APIs, Copilot, and trusted tools call the same use case when the domain behavior is the same. -- Surface adapters authenticate and construct a `Principal`, apply request-rate policy, parse contracts, map input, and present their own result. They never query protected data, decide resource authorization, implement business transactions, or record semantic audit (`check:source-conventions` ratchets value imports of `@sim/db` under `app/api/`). -- Application use cases load canonical context, compare asserted scope, authorize current access, execute managers/repositories, project semantic audit, and trigger shared domain effects. Managers accept canonical IDs and scope, never credentials or principals. Application code stays surface-neutral (`check:boundaries`): never `next/server` or `app/api/**`, and never a runtime import of a route contract object, presenter, or Copilot handler (contract types, schemas, and constants are fine); a surface fact, such as a cursor's route, comes in as input. -- Copilot is a surface adapter. Use `createCopilotApplicationAdapter` and the domain's registered operation object; never a Copilot-only authorization or business implementation. -- Protected compound mutations belong in one top-level semantic application operation, never a sequence of independently committing mutations in a route or tool adapter. -- Never substitute a billing owner, uploader, creator, or API-key owner for the acting principal. Fail fast when the identity model or operation policy cannot express the caller. -- Use the `migrate-application-operation` skill whenever creating or migrating a protected endpoint, tool command, or resource method. - -The `'use client'` server boundary, the app/worker runtime env split, and feature folder layout are in `.claude/rules/sim-architecture.md`. +- Every protected read, write, canonical resource lookup, or authorization-sensitive reference resolution enters through one authorized application use case with its minimum role, workspace-key policy, and allowed principal kinds. Internal APIs, v2 APIs, Copilot (via `createCopilotApplicationAdapter`), and trusted tools call the same use case for the same domain behavior. +- Surface adapters authenticate, build a `Principal`, apply rate policy, parse contracts, and present results. They never query protected data, decide authorization, run business transactions, or record semantic audit (`check:source-conventions` ratchets `@sim/db` imports under `app/api/`). Application code stays surface-neutral (`check:boundaries`). +- A protected compound mutation is one top-level operation, never a sequence of independently committing mutations in an adapter. +- Never substitute a billing owner, uploader, creator, or API-key owner for the acting principal; fail fast when the identity model cannot express the caller. +- Use the `migrate-application-operation` skill for any new or migrated protected endpoint, tool command, or resource method. ## Code Conventions -- **Naming**: components PascalCase (`WorkflowList`); hooks `use*`; files kebab-case (`workflow-list.tsx`); constants SCREAMING_SNAKE_CASE; interfaces PascalCase with a suffix (`WorkflowListProps`); stores `stores//store.ts`. Inside `lib/`, `executor/`, `providers/`, `stores/`, `hooks/`, `serializer/`, and a package's `src/`, a file never starts with its folder's name (`logs/views.ts`, not `logs/log-views.ts`), and a file in `utils/` or `helpers/` never repeats that role (`date.ts`, not `date-utils.ts`); `feature/feature.tsx` stays the component convention. `check:file-names` ratchets this. -- **Imports**: absolute (`@/...`) only, never relative (a barrel `index.ts` re-exports its own siblings relatively). `check:source-conventions` ratchets relative imports in `apps/sim`. 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`. -- **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(null)`). -- **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, or a type parameter kept for API shape. `const { a, ...rest } = obj` to omit keys is allowed. The rules carry no autofix, so `bun run lint` will not rename anything for you. -- **Components**: `'use client'` only for hooks or browser APIs (`check:client-boundary` guards the server boundary). 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. -- **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`. -- **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. -- **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`. -- **Caching**: `lru-cache` with a `max` ceiling, never a hand-rolled TTL `Map`; a lifecycle map is not a cache; cache the gate, never the credential: `.claude/rules/sim-caching.md`. +- **Naming**: components PascalCase, hooks `use*`, files kebab-case, constants SCREAMING_SNAKE_CASE, interfaces with a suffix (`WorkflowListProps`). Inside `lib/`, `executor/`, `providers/`, `stores/`, `hooks/`, `serializer/`, and a package's `src/`, a file never repeats its folder's name or role (`logs/views.ts`, `utils/date.ts`) (`check:file-names`). +- **Imports**: absolute (`@/...`) only; a folder with 3+ exports gets an `index.ts` barrel; `import type` for type-only imports (`.claude/rules/sim-imports.md`). +- **TypeScript**: no `any`, no non-null `!`, no export nothing imports (`check:explicit-any`, `check:unused-exports`). Unused bindings fail biome with no autofix: delete them and update callers; `_` only for a parameter that must hold its position. +- **Utils**: check `lib/` and the `@sim/utils/` helpers before writing a helper. Inline a helper with one consumer; create `utils.ts` (in `lib/` or `feature/utils/`) only when 2+ files share it. +- **State ownership**: React Query owns server data (never `useState` + `fetch`); shareable 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, queries, stores, URL state, components, lists and menus, caching, and render performance each have a rule file in `.claude/rules/`. +- **UI**: Tailwind and the `@sim/emcn` chip family (`.claude/rules/sim-styling.md`); product copy `.claude/rules/sim-ui-copy.md`; marketing copy `.claude/rules/constitution.md`. ## API Contracts and Routes -- Request/response shapes for every route under `apps/sim/app/api/**` live in `apps/sim/lib/api/contracts/**`, built with `defineRouteContract`. Export the contract; export a named schema or type alias only when another module imports it (`check:unused-exports`). Routes never import `zod` or define route-local boundary schemas; clients never write ad-hoc wire types or `z.input`/`z.output`. -- Every route handler runs inside `withRouteHandler`. Ordinary internal and v2 routes use the shared builders (`defineInternalJsonRoute`, `defineV2JsonRoute`, binary/stream variants), which already apply it — never double-wrap. Raw `withRouteHandler` is only for documented protocol or lifecycle exceptions. Never export a bare `async function GET/POST/...` (`check:source-conventions`). -- Same-origin JSON calls go through `requestJson(contract, ...)` from `@/lib/api/client/request`. A raw `fetch` is only for streaming, binary downloads, multipart uploads, signed URLs, OAuth redirects, or external origins, and carries `// boundary-raw-fetch: `. -- The other script-enforced exceptions are `// double-cast-allowed:`, `// boundary-raw-json:`, and `// untyped-response:`. Never add one to silence a fixable finding. -- `bun run check:api-validation:strict` must pass. Full contract rules, route pattern, annotation placement, the end-to-end order, and the schema review checklist: `.claude/rules/sim-api-contracts.md`. React Query hooks (key factories, named `staleTime` constants, `signal`, invalidation, server prefetch): `.claude/rules/sim-queries.md`. - -## Styling and EMCN - -- Tailwind only. Inline `style` only for a genuinely dynamic value or a CSS variable; type size is a named token, never `text-[Npx]` (`check:source-conventions` ratchets both). 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` (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`. +- Request/response shapes for every route under `apps/sim/app/api/**` live in `apps/sim/lib/api/contracts/**` via `defineRouteContract`. Routes never import `zod`; clients never hand-write wire types. +- Handlers use the shared builders (`defineInternalJsonRoute`, `defineV2JsonRoute`, binary/stream variants), which already apply `withRouteHandler`; never double-wrap or export a bare handler. +- Same-origin JSON calls use `requestJson(contract, ...)` from `@/lib/api/client/request`. A raw `fetch` is only for streaming, binary, multipart, signed URLs, OAuth redirects, or external origins, and carries `// boundary-raw-fetch: `. +- `bun run check:api-validation:strict` must pass. Full rules: `.claude/rules/sim-api-contracts.md`. ## Testing -Most unit tests in a codebase like this restate the code they test. They pass on the first run, break on every refactor, and catch nothing that type-check, `next build`, `bun run check:audits`, or a real end-to-end run would miss. Test for confidence, not coverage. +Most unit tests restate the code they test: they pass first time, break on every refactor, and catch nothing type-check, the build, the audits, or a real run would miss. Test for confidence, not coverage. -- **Never write unit tests after you write code.** A test written to describe code that already exists restates the implementation and proves nothing. If the change needs proof, prove it end to end. -- **Highly prefer E2E tests.** Use them to verify complex features work, against the real boundary: real Postgres/Redis (`*.integration.ts`), the running app over real HTTP (`apps/sim/scripts/test-*-e2e.ts`), or the packaged desktop app (`apps/desktop/e2e`, Playwright). At the end of an E2E test, produce a verifiable and repeatable artifact — a JSON report of each check with status and duration, an HTTP status log, a trace, or a screenshot — written to a caller-supplied `_REPORT_PATH` and uploaded by CI on failure. `apps/sim/scripts/test-scim-e2e.ts` is the reference. -- **If you must test a system in isolation, first write down all the ways it could fail, then write the code.** Each failure mode (bad input, boundary, concurrency, partial failure, permission denial, resource cap) becomes one test that fails before the code exists. -- A regression test must fail on the pre-fix code. Revert each guard of the fix and watch its test go red before you trust it. -- Never write tests that restate declarations (block/tool/provider config, registries, constants, schemas accepting valid input), mock every collaborator and assert `toHaveBeenCalledWith` on the mocks, check rendered text or class names, or test mocks and factories themselves. -- Never hand-roll a mock or test helper that `apps/sim/vitest.setup.ts` or `@sim/testing` already provides; a module mocked in a third file gets one central mock. `bun run check:test-patterns` enforces this. +- Never write unit tests after the code to describe it. When a change needs proof, prove it end to end against the real boundary (real Postgres/Redis `*.integration.ts`, the running app over HTTP, or the packaged desktop app), ending in a report artifact. +- Testing in isolation: first list the failure modes, then write one test per mode that fails before the code exists. A regression test must fail on the pre-fix code; revert the fix and watch it go red. +- Never test declarations, mock every collaborator and assert on the mocks, check rendered text or class names, or hand-roll a mock that `apps/sim/vitest.setup.ts` or `@sim/testing` provides (`check:test-patterns`). -Use the `test-audit` skill whenever you write, change, review, or sweep tests — it holds the authoring gate, the junk patterns, and the retention bar. Test layers, file naming, and Vitest mechanics (global mocks, `@sim/testing`, performance rules) are in `.claude/rules/sim-testing.md`. +Use the `test-audit` skill whenever you write, change, or review tests; mechanics are in `.claude/rules/sim-testing.md`. ## Integrations -Build order: **Tools** → **Block** → **Icon** → optional **Trigger**, starting from the service's API docs. Use the skills: `/add-integration` (end-to-end), `/add-tools`, `/add-block`, `/add-trigger`. Two rules the skills assume: - -- **Tool IDs are `snake_case`** (`service_action`), registered in `tools/registry.ts`; blocks register in `blocks/registry-maps.ts` (`BLOCK_REGISTRY` + `BLOCK_META_REGISTRY`, alphabetically). -- **Type coercions go in `tools.config.params`** (runs at execution, after variable resolution), never in `tools.config.tool` (runs at serialization, where `Number()` destroys dynamic `` references). - -A surface that lists integrations to a viewer (a catalog, picker, or `@` menu) reads `useIntegrationAccess` (`hooks/use-integration-access.ts`): the one verdict across permission group, deployment allowlist, and deployment OAuth configuration, which fails closed until the viewer's policy loads. Never map over a raw catalog (`INTEGRATIONS`, `listIntegrations()`, `INTEGRATION_METADATA`) there; `check:integration-access` fails the build. - -Remaining block/tool/trigger rules: `.claude/rules/sim-integrations.md`. Canvas sentences: `apps/sim/blocks/AGENTS.md`. - -## Tables - -Table column types are registry entries in `apps/sim/lib/table/column-types/` — one file per type owning its label, icon, storage cast, coercion, validation, conversion compatibility, formatting, and editor. `Record` on `registry.ts` and `registry.server.ts` is a compile-time completeness gate: adding a type to the union errors until both entries exist. +Build order **Tools** → **Block** → **Icon** → optional **Trigger**, from the service's API docs, using `/add-integration` (or `/add-tools`, `/add-block`, `/add-trigger`). -Never add a `case 'sometype':` outside `column-types/`, except the one documented import-path switch (`coerceValue` in `lib/table/import.ts`), which every new type extends — a missing arm fails silently (a wrong `jsonbCast` breaks every filter on the column). If a consumer needs per-type knowledge, add a registry field. Use `/add-column-type` for the full procedure. +- **Tool IDs are `snake_case`** (`service_action`), registered in `tools/registry.ts`; blocks register alphabetically in `blocks/registry-maps.ts`. +- **Type coercions go in `tools.config.params`** (runs at execution), never `tools.config.tool` (runs at serialization, where `Number()` destroys `` references). +- A surface listing integrations to a viewer reads `useIntegrationAccess`, never a raw catalog (`check:integration-access`). +- Table column types are registry entries in `apps/sim/lib/table/column-types/`; never add a per-type `case` elsewhere. Use `/add-column-type`. ## How your work is checked -Before declaring a change done, run the local gate from the repo root; CI runs these and fails on any of them: +Before declaring a change done, run the local gate from the repo root; CI fails on any of them: ```bash bun run lint # biome format + lint, autofixes (CI runs lint:check) @@ -150,4 +74,4 @@ git fetch origin staging # the block-registry check diffs against it bun run apps/sim/scripts/check-block-registry.ts origin/staging ``` -Author a `packages/db` schema change or migration with the `db-migrate` 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). +Author a `packages/db` schema change or migration with the `db-migrate` skill and run `bun run check:migrations` (not in `check:audits`). Ratchet baselines (`scripts/*baseline.json`) only shrink: regenerate one with `--update` after removing violations. From f8b87fe4b97dc63cfc7e52c1115338d13fe80be1 Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Fri, 6 Nov 2026 11:39:32 -0800 Subject: [PATCH 2/3] chore(agents): restore dropped guidance details and widen rule scopes to match --- .claude/rules/sim-styling.md | 2 ++ .claude/rules/sim-testing.md | 1 + .cursor/rules/sim-styling.mdc | 2 +- .cursor/rules/sim-testing.mdc | 2 +- CLAUDE.md | 26 ++++++++++++++------------ 5 files changed, 19 insertions(+), 14 deletions(-) diff --git a/.claude/rules/sim-styling.md b/.claude/rules/sim-styling.md index 17a563e4b0b..0f349a5c356 100644 --- a/.claude/rules/sim-styling.md +++ b/.claude/rules/sim-styling.md @@ -3,6 +3,8 @@ description: Tailwind CSS and styling conventions paths: - "apps/sim/**/*.tsx" - "apps/sim/**/*.css" + - "packages/workflow-renderer/**/*.tsx" + - "packages/auth-ui/**/*.tsx" --- # Styling Rules diff --git a/.claude/rules/sim-testing.md b/.claude/rules/sim-testing.md index 2559d7620d0..4250aa87088 100644 --- a/.claude/rules/sim-testing.md +++ b/.claude/rules/sim-testing.md @@ -5,6 +5,7 @@ paths: - "**/*.test.tsx" - "**/*.integration.ts" - "**/e2e/**" + - "apps/sim/scripts/test-*-e2e.ts" --- # Testing diff --git a/.cursor/rules/sim-styling.mdc b/.cursor/rules/sim-styling.mdc index 17a82739a0f..56959f469b4 100644 --- a/.cursor/rules/sim-styling.mdc +++ b/.cursor/rules/sim-styling.mdc @@ -1,6 +1,6 @@ --- description: "Tailwind CSS and styling conventions" -globs: ["apps/sim/**/*.tsx","apps/sim/**/*.css"] +globs: ["apps/sim/**/*.tsx","apps/sim/**/*.css","packages/workflow-renderer/**/*.tsx","packages/auth-ui/**/*.tsx"] --- diff --git a/.cursor/rules/sim-testing.mdc b/.cursor/rules/sim-testing.mdc index 9f7f523930d..d4906ad3d77 100644 --- a/.cursor/rules/sim-testing.mdc +++ b/.cursor/rules/sim-testing.mdc @@ -1,6 +1,6 @@ --- description: "Test layers, naming, and Vitest mechanics for Sim" -globs: ["**/*.test.ts","**/*.test.tsx","**/*.integration.ts","**/e2e/**"] +globs: ["**/*.test.ts","**/*.test.tsx","**/*.integration.ts","**/e2e/**","apps/sim/scripts/test-*-e2e.ts"] --- diff --git a/CLAUDE.md b/CLAUDE.md index 9d120e53fc7..dc98057cab4 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -9,30 +9,32 @@ Most rules here are enforced by a `check:*` audit or biome. When one fails, its - **Package manager**: `bun` and `bunx`, never `npm` and `npx`. - **Logging**: `createLogger` from `@sim/logger`, never `console.log`. Messages are static text and ids go in metadata (`logger.info('Sync started', { documentId })`); inside `withRouteHandler` every line already carries the request ID. - **Comments**: name things so the code explains itself. TSDoc on exported APIs and non-obvious modules; an inline `//` only for a terse, non-obvious *why* or a script-enforced `// : ` annotation. History goes in the commit message. No `====` separators, commented-out code, or ESLint directives (`check:comment-hygiene`). -- **Common utilities**: use the `@sim/utils/` helpers (`id`, `random`, `errors`, `object`, `coerce`, `string`, `helpers`, `retry`) over inline versions; `check:utils` bans the inline forms and names the replacement. It cannot catch these, so reach for them yourself: `toArray` and `toStringOrNull`/`toNumberOrNull`/`toBooleanOrNull` for untyped payloads, `compareStrings` (never `localeCompare`) for hashes and cross-process ordering, and `backoffWithJitter`/`parseRetryAfter` for retries. +- **Common utilities**: use the `@sim/utils/` helpers (`id`, `random`, `errors`, `object`, `coerce`, `string`, `helpers`, `retry`) over inline versions. IDs are `generateId()` (default) or `generateShortId()` from `@sim/utils/id`, never `randomUUID`, `nanoid`, or `uuid`: the helpers also work in non-secure (HTTP) browsers. `check:utils` bans the inline forms and names the replacement. It cannot catch these, so reach for them yourself: `toArray` and `toStringOrNull`/`toNumberOrNull`/`toBooleanOrNull` for untyped payloads, `compareStrings` (never `localeCompare`) for hashes and cross-process ordering, and `backoffWithJitter`/`parseRetryAfter` for retries. Where the source is already typed, keep an inline `Array.isArray` check (it narrows; `toArray` asserts), and keep a local helper that deliberately differs (returns `undefined` instead of `null`, or a stricter `Number.isFinite` check). - **Deployment flags in the browser**: client code reads `hosted`, `billingEnabled`, `chatEnabled`, and enterprise flags through `useDeploymentShape()` / `getDeploymentShape()` from `@/lib/core/config/deployment-shape`, never `env-flags` (its values freeze wrong on 404 and error shells). Server code keeps `env-flags` (`check:client-boundary`). -- **Type-checking**: `bun run type-check` at the root checks every workspace. `apps/sim` excludes `*.test.ts(x)` from it; CI's `check:test-types` ratchets their errors per file instead. Keep the unused `@typescript/native` root devDependency (`check:native-typecheck`). +- **Type-checking**: `bun run type-check` at the root checks every workspace (inside a workspace it checks just that one). `apps/sim` excludes `*.test.ts(x)` from it; CI's `check:test-types` ratchets their errors per file instead. Keep the unused `@typescript/native` root devDependency (`check:native-typecheck`). ## Architecture -- **Package boundaries**: `apps/* → packages/*` only; packages never import from `apps/*`. `apps/realtime` (a Bun Socket.IO server) avoids Next.js, React, the block/tool registry, provider SDKs, and the executor, and calls `apps/sim` only over internal HTTP with `INTERNAL_API_SECRET` (`check:boundaries`, `check:realtime-prune`). Both apps share Better Auth sessions through `@sim/db`. +- **Layout**: `apps/sim` is the Next.js app (UI, API routes, executor; use cases live in `lib//application`), `apps/realtime` the collaborative Socket.IO server, `apps/desktop` an Electron shell around the hosted web app, `apps/docs` the docs site, and `apps/pii` a Python service outside the JS/turbo build. Shared code is in `packages/*` (`@sim/*`). +- **Package boundaries**: `apps/* → packages/*` only; packages never import from `apps/*`. `apps/realtime` (a Bun Socket.IO server) avoids Next.js, React, the block/tool registry, provider SDKs, and the executor, and calls `apps/sim` only over internal HTTP with `INTERNAL_API_SECRET` (`check:boundaries`, `check:realtime-prune`). Both apps share Better Auth sessions through `@sim/db` and the same `BETTER_AUTH_SECRET`. - `'use client'` boundary, app/worker env split, and feature folder layout: `.claude/rules/sim-architecture.md`. ### Application Operation Boundary - Every protected read, write, canonical resource lookup, or authorization-sensitive reference resolution enters through one authorized application use case with its minimum role, workspace-key policy, and allowed principal kinds. Internal APIs, v2 APIs, Copilot (via `createCopilotApplicationAdapter`), and trusted tools call the same use case for the same domain behavior. -- Surface adapters authenticate, build a `Principal`, apply rate policy, parse contracts, and present results. They never query protected data, decide authorization, run business transactions, or record semantic audit (`check:source-conventions` ratchets `@sim/db` imports under `app/api/`). Application code stays surface-neutral (`check:boundaries`). +- Surface adapters authenticate, build a `Principal`, apply rate policy, parse contracts, and present results. They never query protected data, decide authorization, run business transactions, or record semantic audit (`check:source-conventions` ratchets value imports of `@sim/db` under `app/api/`). Managers accept canonical IDs and scope, never credentials or principals. Application code stays surface-neutral (`check:boundaries`): no `next/server`, `app/api/**`, or runtime import of a route contract object, presenter, or Copilot handler (contract types, schemas, and constants are fine). - A protected compound mutation is one top-level operation, never a sequence of independently committing mutations in an adapter. - Never substitute a billing owner, uploader, creator, or API-key owner for the acting principal; fail fast when the identity model cannot express the caller. - Use the `migrate-application-operation` skill for any new or migrated protected endpoint, tool command, or resource method. ## Code Conventions -- **Naming**: components PascalCase, hooks `use*`, files kebab-case, constants SCREAMING_SNAKE_CASE, interfaces with a suffix (`WorkflowListProps`). Inside `lib/`, `executor/`, `providers/`, `stores/`, `hooks/`, `serializer/`, and a package's `src/`, a file never repeats its folder's name or role (`logs/views.ts`, `utils/date.ts`) (`check:file-names`). -- **Imports**: absolute (`@/...`) only; a folder with 3+ exports gets an `index.ts` barrel; `import type` for type-only imports (`.claude/rules/sim-imports.md`). -- **TypeScript**: no `any`, no non-null `!`, no export nothing imports (`check:explicit-any`, `check:unused-exports`). Unused bindings fail biome with no autofix: delete them and update callers; `_` only for a parameter that must hold its position. +- **Naming**: components PascalCase, hooks `use*`, files kebab-case, constants SCREAMING_SNAKE_CASE, interfaces with a suffix (`WorkflowListProps`), stores at `stores//store.ts`. Inside `lib/`, `executor/`, `providers/`, `stores/`, `hooks/`, `serializer/`, and a package's `src/`, a file never starts with its folder's name (`logs/views.ts`, not `logs/log-views.ts`; `feature/feature.tsx` stays the component convention), and a file in `utils/` or `helpers/` never repeats that role (`date.ts`, not `date-utils.ts`) (`check:file-names`). +- **Imports**: absolute (`@/...`) only; a folder with 3+ exports gets an `index.ts` barrel, and nothing re-exports from a non-barrel file; `import type` for type-only imports (`.claude/rules/sim-imports.md`). +- **TypeScript**: no `any` (use precise types or `unknown` with guards), no non-null `!`, no export nothing imports (`check:explicit-any`, `check:unused-exports`). Unused bindings fail biome with no autofix: delete them and update callers, and write `catch {}` when the error is unused. `_` only for a parameter that must hold its position or a type parameter kept for API shape; `const { a, ...rest } = obj` is fine. - **Utils**: check `lib/` and the `@sim/utils/` helpers before writing a helper. Inline a helper with one consumer; create `utils.ts` (in `lib/` or `feature/utils/`) only when 2+ files share it. -- **State ownership**: React Query owns server data (never `useState` + `fetch`); shareable 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, queries, stores, URL state, components, lists and menus, caching, and render performance each have a rule file in `.claude/rules/`. +- **Caching**: `lru-cache` with a `max` ceiling, never a hand-rolled TTL `Map`; cache the gate, never the credential (`.claude/rules/sim-caching.md`). +- **State ownership**: React Query owns server data (never `useState` + `fetch`); shareable 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, queries, stores, URL state, components, lists and menus, and render performance each have a rule file in `.claude/rules/`. - **UI**: Tailwind and the `@sim/emcn` chip family (`.claude/rules/sim-styling.md`); product copy `.claude/rules/sim-ui-copy.md`; marketing copy `.claude/rules/constitution.md`. ## API Contracts and Routes @@ -46,8 +48,8 @@ Most rules here are enforced by a `check:*` audit or biome. When one fails, its Most unit tests restate the code they test: they pass first time, break on every refactor, and catch nothing type-check, the build, the audits, or a real run would miss. Test for confidence, not coverage. -- Never write unit tests after the code to describe it. When a change needs proof, prove it end to end against the real boundary (real Postgres/Redis `*.integration.ts`, the running app over HTTP, or the packaged desktop app), ending in a report artifact. -- Testing in isolation: first list the failure modes, then write one test per mode that fails before the code exists. A regression test must fail on the pre-fix code; revert the fix and watch it go red. +- Never write unit tests after the code: a test written to describe existing code restates it and proves nothing. When a change needs proof, prove it end to end against the real boundary (real Postgres/Redis `*.integration.ts`, the running app over HTTP (`apps/sim/scripts/test-*-e2e.ts`), or the packaged desktop app), ending in a report artifact written to a caller-supplied `_REPORT_PATH` (`apps/sim/scripts/test-scim-e2e.ts` is the reference). +- Testing in isolation: first list the failure modes (bad input, boundary, concurrency, partial failure, permission denial, resource cap), then write one test per mode that fails before the code exists. A regression test must fail on the pre-fix code: revert each guard of the fix and watch its test go red. - Never test declarations, mock every collaborator and assert on the mocks, check rendered text or class names, or hand-roll a mock that `apps/sim/vitest.setup.ts` or `@sim/testing` provides (`check:test-patterns`). Use the `test-audit` skill whenever you write, change, or review tests; mechanics are in `.claude/rules/sim-testing.md`. @@ -59,7 +61,7 @@ Build order **Tools** → **Block** → **Icon** → optional **Trigger**, from - **Tool IDs are `snake_case`** (`service_action`), registered in `tools/registry.ts`; blocks register alphabetically in `blocks/registry-maps.ts`. - **Type coercions go in `tools.config.params`** (runs at execution), never `tools.config.tool` (runs at serialization, where `Number()` destroys `` references). - A surface listing integrations to a viewer reads `useIntegrationAccess`, never a raw catalog (`check:integration-access`). -- Table column types are registry entries in `apps/sim/lib/table/column-types/`; never add a per-type `case` elsewhere. Use `/add-column-type`. +- Table column types are registry entries in `apps/sim/lib/table/column-types/`; never add a per-type `case` elsewhere except the one documented import-path switch (`coerceValue` in `lib/table/import.ts`), which every new type extends: a missed arm fails silently. Use `/add-column-type`. ## How your work is checked @@ -74,4 +76,4 @@ git fetch origin staging # the block-registry check diffs against it bun run apps/sim/scripts/check-block-registry.ts origin/staging ``` -Author a `packages/db` schema change or migration with the `db-migrate` skill and run `bun run check:migrations` (not in `check:audits`). Ratchet baselines (`scripts/*baseline.json`) only shrink: regenerate one with `--update` after removing violations. +Author a `packages/db` schema change or migration with the `db-migrate` skill and run `bun run check:migrations` (not in `check:audits`). Ratchet baselines (`scripts/*baseline.json`) only shrink: regenerate one with `--update` after removing violations. The one exception is `check:tool-registry-boundary`, whose module-count baseline is re-recorded with `--update-baseline` when growth is deliberate (see the `tool-registry-boundary` skill). From 5b3c854ec1b9bf8363f23527ba57a3c413244072 Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Fri, 6 Nov 2026 11:45:21 -0800 Subject: [PATCH 3/3] chore(agents): keep auth-ui off the emcn styling rule --- .claude/rules/sim-styling.md | 1 - .cursor/rules/sim-styling.mdc | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/.claude/rules/sim-styling.md b/.claude/rules/sim-styling.md index 0f349a5c356..2c5e16be354 100644 --- a/.claude/rules/sim-styling.md +++ b/.claude/rules/sim-styling.md @@ -4,7 +4,6 @@ paths: - "apps/sim/**/*.tsx" - "apps/sim/**/*.css" - "packages/workflow-renderer/**/*.tsx" - - "packages/auth-ui/**/*.tsx" --- # Styling Rules diff --git a/.cursor/rules/sim-styling.mdc b/.cursor/rules/sim-styling.mdc index 56959f469b4..f016cfc5b3f 100644 --- a/.cursor/rules/sim-styling.mdc +++ b/.cursor/rules/sim-styling.mdc @@ -1,6 +1,6 @@ --- description: "Tailwind CSS and styling conventions" -globs: ["apps/sim/**/*.tsx","apps/sim/**/*.css","packages/workflow-renderer/**/*.tsx","packages/auth-ui/**/*.tsx"] +globs: ["apps/sim/**/*.tsx","apps/sim/**/*.css","packages/workflow-renderer/**/*.tsx"] ---