{"owner":"simstudioai","repo":"sim","hasSkills":true,"hasMcp":false,"mcpConfig":null,"found":["CLAUDE.md","AGENTS.md"],"files":{"CLAUDE.md":"# Sim Development Guidelines\n\nYou are a professional software engineer. All code must follow best practices: accurate, readable, clean, and efficient.\n\n## Global Standards\n\n- **Linting / Audit**: `bun run check:api-validation` must pass on PRs. Do not introduce route-local boundary Zod schemas, direct route Zod imports, or ad-hoc client wire types — see \"API Contracts\" and \"API Route Pattern\" below\n- **Logging**: Import `createLogger` from `@sim/logger`. Use `logger.info`, `logger.warn`, `logger.error` instead of `console.log`. Inside API routes wrapped with `withRouteHandler`, loggers automatically include the request ID — no manual `withMetadata({ requestId })` needed\n- **API Route Handlers**: All API route handlers (`GET`, `POST`, `PUT`, `DELETE`, `PATCH`) must run inside `withRouteHandler`. Ordinary internal and v2 handlers use the shared JSON/binary route builders, which already apply it; never double-wrap a builder. Use raw `withRouteHandler` only for documented protocol or lifecycle exceptions. See \"API Route Pattern\" below\n- **Comments**: Use TSDoc for documentation. No `====` separators. No non-TSDoc comments\n- **Styling**: Never update global styles. Keep all styling local to components\n- **ID Generation**: Never use `crypto.randomUUID()`, `nanoid`, or `uuid` package. Use `generateId()` (UUID v4) or `generateShortId()` (compact) from `@sim/utils/id`\n- **Common Utilities**: Use shared helpers from `@sim/utils` instead of inline implementations:\n  - `sleep(ms)` from `@sim/utils/helpers` — never `new Promise(resolve => setTimeout(resolve, ms))`\n  - `toError(e)` from `@sim/utils/errors` — normalize caught values to `Error`\n  - `getErrorMessage(e, fallback?)` from `@sim/utils/errors` — extract message string from unknown caught value; never write `e instanceof Error ? e.message : 'fallback'`\n  - `structuredClone(value)` — built-in deep clone; never `JSON.parse(JSON.stringify(...))`\n  - `omit(obj, keys)` / `filterUndefined(obj)` from `@sim/utils/object` — object trimming; never `Object.fromEntries(Object.entries(...).filter(...))`\n  - `truncate(str, maxLength, suffix?)` from `@sim/utils/string` — never inline slice + ellipsis\n  - `backoffWithJitter(attempt, retryAfterMs, options?)` / `parseRetryAfter(header)` from `@sim/utils/retry` — shared retry pacing; never reimplement exponential backoff inline\n- **Package Manager**: Use `bun` and `bunx`, not `npm` and `npx`\n- **Type-checking**: Run `bun run type-check` (per workspace) or `bunx turbo run type-check` (all of them). Do not remove the `@typescript/native` alias from the root `devDependencies` — nothing imports it, but it is what makes a bare `tsc` resolve to the native TypeScript 7 compiler instead of the ~10x slower JavaScript TypeScript 6 one that `@typescript/typescript6` pulls in transitively. `bun run check:native-typecheck` enforces this\n\n## Architecture\n\n### Core Principles\n\n1. Single Responsibility: Each component, hook, store has one clear purpose\n2. Composition Over Complexity: Break down complex logic into smaller pieces\n3. Type Safety First: TypeScript interfaces for all props, state, return types\n4. Predictable State: Zustand for global state, useState for UI-only concerns\n\n### Application Operation Boundary\n\n- Every protected read, write, canonical resource lookup, or authorization-sensitive reference resolution enters through an authorized application use case.\n- 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.\n- Surface adapters authenticate and construct a `Principal`, apply request-rate policy, parse contracts, map input, and present results. They never query protected data, decide resource authorization, implement business transactions, or record semantic audit.\n- 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.\n- Copilot is a surface adapter. Use `createCopilotApplicationAdapter` and the domain's registered operation object; do not create Copilot-only authorization or business implementations.\n- Protected compound mutations belong in one top-level semantic application operation. Do not sequence independently committing mutations in a route or tool adapter.\n- 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.\n- Use the `migrate-application-operation` skill whenever creating or migrating a protected endpoint, tool command, or resource method.\n\n### Root Structure\n\n```\napps/\n├── sim/                    # Next.js app (UI + API routes + workflow editor)\n│   ├── app/                # Next.js app router (pages, API routes)\n│   ├── blocks/             # Block definitions and registry\n│   ├── components/         # Shared UI (emcn/, ui/)\n│   ├── executor/           # Workflow execution engine\n│   ├── hooks/              # Shared hooks (queries/, selectors/)\n│   ├── lib/                # App-wide utilities\n│   ├── providers/          # LLM provider integrations\n│   ├── stores/             # Zustand stores\n│   ├── tools/              # Tool definitions\n│   └── triggers/           # Trigger definitions\n└── realtime/               # Bun Socket.IO server (collaborative canvas)\n\npackages/\n├── audit/                  # @sim/audit\n├── auth/                   # @sim/auth — shared Better Auth verifier\n├── db/                     # @sim/db — drizzle schema + client\n├── logger/                 # @sim/logger\n├── platform-authz/         # @sim/platform-authz — workspace + workflow authz (subpath exports)\n├── realtime-protocol/      # @sim/realtime-protocol — socket op constants + zod schemas\n├── security/               # @sim/security — safeCompare\n├── tsconfig/               # shared tsconfig presets\n├── utils/                  # @sim/utils\n├── workflow-persistence/   # @sim/workflow-persistence\n└── workflow-types/         # @sim/workflow-types — pure BlockState/Loop/Parallel types\n```\n\n### Package boundaries\n\n- `apps/* → packages/*` only. Packages never import from `apps/*`.\n- `apps/realtime` intentionally avoids Next.js, React, the block/tool registry, provider SDKs, and the executor. Do not add imports from `@/lib/webhooks/providers/*`, `@/executor/*`, `@/blocks/*`, or `@/tools/*` to any package consumed by `apps/realtime`. CI enforces this via `scripts/check-monorepo-boundaries.ts` and `scripts/check-realtime-prune-graph.ts`.\n- Auth is shared across both apps via the Better Auth \"Shared Database Session\" pattern (same `BETTER_AUTH_SECRET`, same DB via `@sim/db`).\n\n### Naming Conventions\n\n- Components: PascalCase (`WorkflowList`)\n- Hooks: `use` prefix (`useWorkflowOperations`)\n- Files: kebab-case (`workflow-list.tsx`)\n- Stores: `stores/feature/store.ts`\n- Constants: SCREAMING_SNAKE_CASE\n- Interfaces: PascalCase with suffix (`WorkflowListProps`)\n\n## Imports\n\n**Always use absolute imports.** Never use relative imports.\n\n```typescript\n// ✓ Good\nimport { useWorkflowStore } from '@/stores/workflows/store'\n\n// ✗ Bad\nimport { useWorkflowStore } from '../../../stores/workflows/store'\n```\n\nUse barrel exports (`index.ts`) when a folder has 3+ exports. Do not re-export from non-barrel files; import directly from the source.\n\n### Import Order\n\n1. React/core libraries\n2. External libraries\n3. UI components (`@sim/emcn`, `@/components/ui`)\n4. Utilities (`@/lib/...`)\n5. Stores (`@/stores/...`)\n6. Feature imports\n7. CSS imports\n\nUse `import type { X }` for type-only imports.\n\n## TypeScript\n\n1. No `any` - Use proper types or `unknown` with type guards\n2. Always define props interface for components\n3. `as const` for constant objects/arrays\n4. Explicit ref types: `useRef<HTMLDivElement>(null)`\n\n## Components\n\n```typescript\n'use client' // Only if using hooks\n\nconst CONFIG = { SPACING: 8 } as const\n\ninterface ComponentProps {\n  requiredProp: string\n  optionalProp?: boolean\n}\n\nexport function Component({ requiredProp, optionalProp = false }: ComponentProps) {\n  // Order: refs → external hooks → store hooks → custom hooks → state → useMemo → useCallback → useEffect → return\n}\n```\n\nExtract when: 50+ lines, used in 2+ files, or has own state/logic. Keep inline when: < 10 lines, single use, purely presentational.\n\nBehavior-preserving render-performance idioms — lazy-init object refs, hoist closure-free values/functions to module scope, pre-index repeated lookups with `Map`/`Set`, and never mutating a shared array in place — are in `.claude/rules/sim-react-performance.md` (which also explains why `toSorted`/`toReversed` are unsafe on client render paths despite the ES2023 tsconfig lib — SWC does not polyfill prototype methods, so use `[...arr].sort()`). For the render-timing effect/state anti-patterns use the `/you-might-not-need-*` skills and verify against the running UI.\n\n## API Contracts\n\nBoundary HTTP request and response shapes for all routes under `apps/sim/app/api/**` live in `apps/sim/lib/api/contracts/**` (one file per resource family — `folders.ts`, `chats.ts`, `knowledge.ts`, etc.). Routes never define route-local boundary Zod schemas, and clients never define ad-hoc wire types — both sides consume the same contract.\n\n- Each contract is built with `defineRouteContract({ method, path, params?, query?, body?, headers?, response: { mode: 'json', schema } })` from `@/lib/api/contracts`\n- Contracts export named schemas (e.g., `createFolderBodySchema`) AND named TypeScript type aliases (e.g., `export type CreateFolderBody = z.input<typeof createFolderBodySchema>`)\n- Clients (hooks, utilities, components) import the named type aliases from the contract file. They must never write `z.input<...>` / `z.output<...>` themselves\n- Shared identifier schemas live in `apps/sim/lib/api/contracts/primitives.ts` (e.g., `workspaceIdSchema`, `workflowIdSchema`). Reuse these instead of redefining string-based ID schemas\n- Audit script: `bun run check:api-validation` enforces boundary policy and prints ratchet metrics for route Zod imports, route-local schema constructors, route `ZodError` references, client hook Zod imports, and related counters. It must pass on PRs. `bun run check:api-validation:strict` is the strict CI gate and additionally fails on annotations with empty reasons\n\nDomain validators that are not HTTP boundaries — tools, blocks, triggers, connectors, realtime handlers, and internal helpers — may still use Zod directly. The contract rule is boundary-only.\n\n### Boundary annotations\n\nA small number of legitimate exceptions to the boundary rules are tolerated when annotated. The audit script recognizes four annotation forms:\n\n- `// boundary-raw-fetch: <reason>` — placed on the line directly above a raw `fetch(` call in client hooks (`apps/sim/hooks/queries/**`, `apps/sim/hooks/selectors/**`) AND any same-origin `/api/...` fetch elsewhere under `apps/sim/**` outside an API route handler. Use only for documented exceptions: streaming responses, binary downloads, multipart uploads, signed-URL flows, OAuth redirects, and external-origin requests\n- `// double-cast-allowed: <reason>` — placed on the line directly above an `as unknown as X` cast outside test files\n- `// boundary-raw-json: <reason>` — placed on the line directly above a raw `await request.json()` / `await req.json()` read in a route handler. Use only when the body is a JSON-RPC envelope, a tolerant `.catch(() => ({}))` parse, or otherwise cannot go through `parseRequest`\n- `// untyped-response: <reason>` — placed on the line directly above a `schema: z.unknown()` response declaration in a contract file. Use only when the response body is genuinely opaque (user-supplied data, third-party passthrough)\n\nPlacement rule: the annotation must immediately precede the call or cast. Up to three non-empty preceding comment lines are tolerated, so additional context comments above the annotation are fine. The reason must be non-empty after trimming — annotations with empty reasons fail strict mode (`annotationsMissingReason`).\n\nWhole-file allowlists for routes (legitimate non-boundary or auth-handled routes that legitimately import Zod for non-boundary reasons) go through `INDIRECT_ZOD_ROUTES` in `scripts/check-api-validation-contracts.ts`, not per-line annotations.\n\nExamples:\n\n```ts\n// boundary-raw-fetch: streaming SSE chunks must be processed as they arrive\nconst response = await fetch(`/api/copilot/chat/stream?chatId=${chatId}`, { signal })\n```\n\n```ts\n// double-cast-allowed: legacy provider type lacks the discriminator field we need\nconst provider = config as unknown as LegacyProvider\n```\n\n## API Route Pattern\n\nEvery route method must run inside `withRouteHandler`. Ordinary internal and v2 JSON/binary routes use `defineInternalJsonRoute`, `defineV2JsonRoute`, or the matching binary/stream builder. These builders already apply `withRouteHandler`; never wrap them again. Use raw `withRouteHandler` only for explicit protocol or lifecycle exceptions such as streaming, multipart control, large-body admission, OAuth, or public execution.\n\nRoutes never `import { z } from 'zod'` and never define route-local boundary schemas. Declarative builders consume contracts and own authentication, admission, parsing, use-case execution, response validation, and error projection. A raw special route consumes the same contracts and validates with canonical helpers from `@/lib/api/server`, after authentication and cheap admission:\n\n- `parseRequest(contract, request, context, options?)` — fully contract-bound routes; parses params, query, body, and headers in one call. Pass `{}` for `context` on routes without route params, or the route's `context` argument when route params exist. Returns a discriminated union; check `parsed.success` and return `parsed.response` on failure\n- `validationErrorResponse(error)` and `getValidationErrorMessage(error, fallback)` — produce 400 responses from a `ZodError`\n- `validationErrorResponseFromError(error)` — when handling unknown caught errors that may or may not be a `ZodError`\n- `isZodError(error)` — type guard. Routes never use `instanceof z.ZodError`\n\n### Ordinary authorized JSON route\n\n```typescript\nexport const PATCH = defineInternalJsonRoute({\n  contract: renameWidgetContract,\n  auth: internalSessionAuth,\n  operation: widgetOperations.rename,\n  rateLimit: internalRateLimits.none({ reason: 'Preserve existing internal behavior' }),\n  errorPolicy: internalWidgetErrorPolicy,\n  mapInput: ({ params, body }) => ({\n    widgetId: params.widgetId,\n    assertedWorkspaceId: params.workspaceId,\n    name: body.name,\n  }),\n  useCase: renameWidget,\n  present: ({ widget }) => ({ success: true, widget }),\n})\n```\n\nThe contract, operation, and use case must agree at definition time. Authentication and request-rate admission happen before parsing; canonical loading and authorization happen in the application use case. The presenter returns only the surface success body.\n\nRoutes under `apps/sim/app/api/v1/**` use the shared middleware in `apps/sim/app/api/v1/middleware.ts` for auth, rate-limit, and workspace access. Compose contract validation inside that middleware — never reimplement auth/rate-limit per-route.\n\nNever export a bare `async function GET/POST/...`. Export the result of a shared builder or, for a documented special route, `withRouteHandler(...)`.\n\n### Adding a new boundary feature end-to-end\n\nWhen adding a new route + client surface, follow this order. Each step has one place it lives.\n\n1. **Author the contract first** in `apps/sim/lib/api/contracts/<domain>.ts` (or a subdirectory for large domains: `knowledge/`, `selectors/`, `tools/`). Define one schema per request slice (`params`, `query`, `body`, `headers`) and one for the response, then wrap with `defineRouteContract`. Export named type aliases (`z.input` for inputs, `z.output` for outputs).\n2. **Define the semantic operation and application use case** under `apps/sim/lib/<domain>/application/`. The use case owns canonical loading, asserted-scope checks, current authorization, business behavior, semantic audit, and shared domain effects.\n3. **Implement the route adapter** in `apps/sim/app/api/<path>/route.ts` with the appropriate shared builder. Declare auth, operation, rate policy, error policy, input mapping, use case, and presenter. Auth always runs **before** parsing. Use raw `withRouteHandler` only for an explicit special route, and keep protected work in application use cases.\n4. **Add the React Query hook** in `apps/sim/hooks/queries/<domain>.ts`. Use `requestJson(contract, input)` for the call. Build a hierarchical query-key factory (`all` → `lists()` → `list(workspaceId)` → `details()` → `detail(id)`) so invalidations can target prefixes.\n5. **Use the hook in the component**. The mutation's `data` and `error` are fully typed from the contract; surface `error.message` (already extracted from the response body's `error` or `message` field by `requestJson`).\n\n### Schema review checklist (read the contract diff like a DB migration)\n\nLLMs will write contracts that compile but are sloppy. The human reviewer should optimize attention on:\n\n- **`required` vs `optional` vs `nullable` is correct**. `optional()` allows omission; `nullable()` allows `null`; chaining both creates a tri-state that's almost never what you want.\n- **Response schema matches the route's actual JSON output**. The most common drift bug — route emits a field the schema doesn't declare, or omits a required field. Walk every `NextResponse.json(...)` callsite against the schema.\n- **Error messages are descriptive**. `'fileName cannot be empty'` beats `'Required'`. Use the second arg of `min(1, '...')`, `nonempty('...')`, etc. For cross-field refines, use `superRefine` with a `path` and a message that names the failing field.\n- **Bounds are set** on arrays (`.min(1)`, `.max(N)`), strings (`.min(1).max(N)` for IDs/names), and numbers (`.min().max()` for limits/sizes).\n- **`z.unknown()` is a smell** unless the data is genuinely arbitrary (provider passthrough, user-defined tool result, JSON-RPC envelope). When kept, must be annotated `// untyped-response: <specific reason>` in a `schema:` slot.\n- **Discriminated unions over plain unions** when the wire has a discriminant field — gives clients exhaustive narrowing.\n\nCI (`bun run check:api-validation:strict`) catches structural violations (Zod imports in routes, raw `request.json()`, double casts, missing annotations). It does **not** catch these schema-quality judgments — that's the human's job in PR review.\n\n## Hooks\n\n```typescript\ninterface UseFeatureProps { id: string }\n\nexport function useFeature({ id }: UseFeatureProps) {\n  const idRef = useRef(id)\n  const [data, setData] = useState<Data | null>(null)\n  \n  useEffect(() => { idRef.current = id }, [id])\n  \n  const fetchData = useCallback(async () => { ... }, []) // Empty deps when using refs\n  \n  return { data, fetchData }\n}\n```\n\n## Zustand Stores\n\nStores live in `stores/`. Complex stores split into `store.ts` + `types.ts`.\n\n```typescript\nimport { create } from 'zustand'\nimport { devtools } from 'zustand/middleware'\n\nconst initialState = { items: [] as Item[] }\n\nexport const useFeatureStore = create<FeatureState>()(\n  devtools(\n    (set, get) => ({\n      ...initialState,\n      setItems: (items) => set({ items }),\n      reset: () => set(initialState),\n    }),\n    { name: 'feature-store' }\n  )\n)\n```\n\nUse `devtools` middleware. Use `persist` only when data should survive reload with `partialize` to persist only necessary state.\n\n## React Query\n\nAll React Query hooks live in `hooks/queries/`. All server state must go through React Query — never use `useState` + `fetch` in components for data fetching or mutations.\n\n### Client Boundary\n\nHooks consume contracts the same way routes do. Every same-origin JSON call must go through `requestJson(contract, ...)` from `@/lib/api/client/request` instead of raw `fetch`:\n\n- Hooks import named type aliases from `@/lib/api/contracts/**`. Never write `z.input<...>` / `z.output<...>` in hooks, and never `import { z } from 'zod'` in client code\n- `requestJson` parses params, query, body, and headers against the contract on the way out and validates the JSON response on the way back. Hooks always forward `signal` for cancellation\n- Documented exceptions for raw `fetch`: streaming responses, binary downloads, multipart uploads, signed-URL flows, OAuth redirects, and external-origin requests. Mark each raw `fetch` with a TSDoc comment explaining which exception applies. The `// boundary-raw-fetch` annotation is required not only in client hooks but for any same-origin `/api/...` fetch anywhere under `apps/sim/**` outside an API route handler — strict CI flags these regardless of location\n\n```typescript\nimport { keepPreviousData, useQuery } from '@tanstack/react-query'\nimport { requestJson } from '@/lib/api/client/request'\nimport { listEntitiesContract, type EntityList } from '@/lib/api/contracts/entities'\n\nexport const ENTITY_LIST_STALE_TIME = 60 * 1000\n\nasync function fetchEntities(workspaceId: string, signal?: AbortSignal): Promise<EntityList> {\n  const data = await requestJson(listEntitiesContract, {\n    query: { workspaceId },\n    signal,\n  })\n  return data.entities\n}\n\nexport function useEntityList(workspaceId?: string) {\n  return useQuery({\n    queryKey: entityKeys.list(workspaceId),\n    queryFn: ({ signal }) => fetchEntities(workspaceId as string, signal),\n    enabled: Boolean(workspaceId),\n    staleTime: ENTITY_LIST_STALE_TIME,\n    placeholderData: keepPreviousData,\n  })\n}\n```\n\n### Query Key Factory\n\nEvery file must have a hierarchical key factory with an `all` root key and intermediate plural keys for prefix invalidation:\n\n```typescript\nexport const entityKeys = {\n  all: ['entity'] as const,\n  lists: () => [...entityKeys.all, 'list'] as const,\n  list: (workspaceId?: string) => [...entityKeys.lists(), workspaceId ?? ''] as const,\n  details: () => [...entityKeys.all, 'detail'] as const,\n  detail: (id?: string) => [...entityKeys.details(), id ?? ''] as const,\n}\n```\n\n### Query Hooks\n\n- Every `queryFn` must forward `signal` for request cancellation\n- Every query must have an explicit `staleTime`, assigned from a named exported constant, never an inline numeric literal — a server-side prefetch hydrating the same query key must import and reuse that constant so the two never drift out of sync\n- Use `keepPreviousData` only on variable-key queries (where params change), never on static keys\n\n```typescript\nexport function useEntityList(workspaceId?: string) {\n  return useQuery({\n    queryKey: entityKeys.list(workspaceId),\n    queryFn: ({ signal }) => fetchEntities(workspaceId as string, signal),\n    enabled: Boolean(workspaceId),\n    staleTime: ENTITY_LIST_STALE_TIME,\n    placeholderData: keepPreviousData, // OK: workspaceId varies\n  })\n}\n```\n\n### Mutation Hooks\n\n- Use targeted invalidation (`entityKeys.lists()`) not broad (`entityKeys.all`) when possible\n- For optimistic updates: use `onSettled` (not `onSuccess`) for cache reconciliation — `onSettled` fires on both success and error\n- Don't include mutation objects in `useCallback` deps — `.mutate()` is stable in TanStack Query v5\n\n```typescript\nexport function useUpdateEntity() {\n  const queryClient = useQueryClient()\n  return useMutation({\n    mutationFn: async (variables) => { /* ... */ },\n    onMutate: async (variables) => {\n      await queryClient.cancelQueries({ queryKey: entityKeys.detail(variables.id) })\n      const previous = queryClient.getQueryData(entityKeys.detail(variables.id))\n      queryClient.setQueryData(entityKeys.detail(variables.id), /* optimistic */)\n      return { previous }\n    },\n    onError: (_err, variables, context) => {\n      queryClient.setQueryData(entityKeys.detail(variables.id), context?.previous)\n    },\n    onSettled: (_data, _error, variables) => {\n      queryClient.invalidateQueries({ queryKey: entityKeys.lists() })\n      queryClient.invalidateQueries({ queryKey: entityKeys.detail(variables.id) })\n    },\n  })\n}\n```\n\n## URL / Query-Param State\n\nShareable *client* view-state (active tab/panel, filters, search query, pagination, selected entity id, view mode, a deep-linked drawer/modal) lives in the URL via [`nuqs`](https://nuqs.dev) — not in a store synced with effects, and never read via `useSearchParams().get(...)` / `new URLSearchParams(window.location.search)`. Remote data stays in React Query; high-frequency / large / ephemeral / socket-synced state stays in Zustand (canvas pan/zoom, cursor, drag, resize widths, live collaborative selection).\n\nCo-locate a `search-params.ts` per feature exporting the parser map (single source of truth, shared by client `useQueryStates`/`useQueryState` and server `createSearchParamsCache`). Never `import { z }` in client code for params — use nuqs parsers. Full decision framework, conventions, the debounced-input pattern, and the workflow-editor carve-out are in `.claude/rules/sim-url-state.md`.\n\n## List & Menu Ordering\n\nA list orders itself the way the user already reads the same things somewhere else. Resource menus (`+` attach, `@` mention, resource-tab `+`) mirror the **sidebar** top-down; a row or root **context menu** mirrors that surface's **toolbar**, left-to-right becoming top-to-bottom; tab strips mirror their nav. Platform-only entries (desktop Browser, Terminal) trail the shared set.\n\nEncode the order in ONE exported constant and sort by it — never a hand-maintained literal per menu (`RESOURCE_MENU_ORDER` / `byResourceMenuOrder` in `home/components/mothership-view/components/resource-registry`). Render mixed item kinds in a single ordered pass; emitting all submenu-backed families and then all flat ones silently pins every submenu to the top no matter what the constant says. Divergence is allowed only for search ranking, user-controlled ordering, and recency. Full rule in `.claude/rules/sim-list-ordering.md`.\n\n## Styling\n\nUse Tailwind only, no inline styles. Use `cn()` from `@sim/emcn` for conditional classes.\n\n```typescript\n<div className={cn('base-classes', isActive && 'active-classes')} />\n```\n\nFor equal height and width, use the `size-*` shorthand — never `h-[Npx] w-[Npx]` or `h-N w-N`. Default icon size is `size-[14px]`.\n\n```typescript\n<Icon className='size-[14px] text-[var(--text-icon)]' />\n```\n\nOn chip components (see \"EMCN Components\"), drive chrome through PROPS, not `className`: `error` for the error state, `icon`/`endAdornment` for adornments, `inputClassName` for the inner field. `className` carries ONLY layout/sizing — never re-specify canonical chrome (border, fill, radius, height, text/icon color) or add focus rings. Full consumer rules in `.claude/rules/sim-styling.md`.\n\n## EMCN Components\n\nImport 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. Use CVA only when 2+ genuine variants exist; otherwise plain `cn()`.\n\nThe chip family is the canonical UI chrome and is progressively replacing the legacy EMCN primitives — always reach for the chip equivalent: `ChipInput` over `Input`, `ChipTextarea` over `Textarea`, `ChipModal`/`ChipModalField` over `Modal`, `ChipSelect`/`ChipCombobox` (searchable) or `ChipDropdown` (simple menu-select) over `Select`/`Combobox`, `ChipSwitch` over `Switch`, `ChipDatePicker` over a raw date field, `Chip`/`ChipLink` for pill buttons/links, `ChipTag` for inline tags/badges. For context/action menus the canonical control is `DropdownMenu` (not a chip, but the standard menu — not a hand-rolled popover). Components OWN their chrome (single source of truth) — consumers pass props, not class overrides. Authoring rules in `.claude/rules/emcn-components.md`; consumer rules in `.claude/rules/sim-styling.md`.\n\nInside a `ChipModalBody`, EVERY labeled field MUST be a `ChipModalField` — never hand-roll a field row (a raw `<div>` + a hand-rolled `<p>`/`<label>` title + a bare `ChipInput`/`ChipTextarea`). `ChipModalBody` applies `px-2` + `gap-4`; `ChipModalField` adds ANOTHER `px-2`, so each field lands at effective `px-4`, exactly matching `ChipModalHeader`/`ChipModalFooter` (`px-4`). Hand-rolled rows skip the field's gutter and sit at `px-2`, visibly misaligned with the header/footer. For controls `ChipModalField` does not cover (`ChipCombobox`, `ChipSelect`, `DatePicker`, `TimePicker`, `ButtonGroup`, arbitrary JSX), use `ChipModalField type='custom'` with a `title` — it still applies the `px-2` gutter and renders the canonical `Label`. Drive intent via props (`title`/`value`/`onChange`/`error`/`hint`/`required`/`flush`); never pass `variant`/`className`/`id` to the inner control, and never add a body-level wrapper `<div>` with a custom `gap-*` that fights `ChipModalBody`'s `gap-4`.\n\n## Design-System Consolidation\n\nPrinciples when building or migrating shared UI:\n\n- One canonical source of truth for shared chrome — compose it, never re-derive it per consumer.\n- Props-driven API over `className` overrides — reaching for `className` to change chrome is a smell; expose a prop instead.\n- Discriminated-union props for modes (e.g. `ChipDropdown multiple`) over near-duplicate components.\n- Delete legacy variants/components after migration — no parallel paths left behind.\n- Plain `cn()` for a single error/state toggle; CVA only for genuinely multiple variants.\n- Align consumers to the canonical defaults — normal weight, `--text-body` text, `--text-icon` icons.\n- Verify referenced CSS vars exist — an undefined var silently falls back to `currentColor` (black-bug).\n\n## Testing\n\nUse Vitest. Test files: `feature.ts` → `feature.test.ts`. See `.cursor/rules/sim-testing.mdc` for full details.\n\n### Global Mocks (vitest.setup.ts)\n\n`@sim/db`, `@sim/db/schema`, `drizzle-orm`, `@sim/logger`, `@sim/platform-authz/workflow`, `@/blocks/registry`, `@/lib/auth`, `@/lib/auth/hybrid`, `@/lib/core/utils/request`, `@trigger.dev/sdk`, and store mocks are provided globally. Do NOT re-mock them unless overriding behavior. (The `vi.mock('@/lib/auth', ...)` in the example below is an override of the global mock so `getSession` can be controlled per-test.)\n\n### Standard Test Pattern\n\n```typescript\n/**\n * @vitest-environment node\n */\nimport { createMockRequest } from '@sim/testing'\nimport { beforeEach, describe, expect, it, vi } from 'vitest'\n\nconst { mockGetSession } = vi.hoisted(() => ({\n  mockGetSession: vi.fn(),\n}))\n\nvi.mock('@/lib/auth', () => ({\n  auth: { api: { getSession: vi.fn() } },\n  getSession: mockGetSession,\n}))\n\nimport { GET } from '@/app/api/my-route/route'\n\ndescribe('my route', () => {\n  beforeEach(() => {\n    vi.clearAllMocks()\n    mockGetSession.mockResolvedValue({ user: { id: 'user-1' } })\n  })\n  it('returns data', async () => { ... })\n})\n```\n\n### Performance Rules\n\n- **NEVER** use `vi.resetModules()` + `vi.doMock()` + `await import()` — use `vi.hoisted()` + `vi.mock()` + static imports\n- **NEVER** use `vi.importActual()` — mock everything explicitly\n- **NEVER** use `mockAuth()`, `mockConsoleLogger()`, `setupCommonApiMocks()` from `@sim/testing` — they use `vi.doMock()` internally\n- **Mock heavy deps** (`@/blocks`, `@/tools/registry`, `@/triggers`) in tests that don't need them\n- **Use `@vitest-environment node`** unless DOM APIs are needed (`window`, `document`, `FormData`)\n- **Avoid real timers** — use 1ms delays or `vi.useFakeTimers()`\n\nUse `@sim/testing` mocks/factories over local test data.\n\n## Utils Rules\n\n- Never create `utils.ts` for single consumer - inline it\n- Create `utils.ts` when 2+ files need the same helper\n- Check existing sources in `lib/` before duplicating\n\n## Adding Integrations\n\nNew integrations are built in order: **Tools** → **Block** → **Icon** → (optional) **Trigger**. Always look up the service's API docs first.\n\nTwo hard rules that the skills assume:\n\n- **Tool IDs are `snake_case`** (`service_action`) and must be registered in `tools/registry.ts`; blocks register in `blocks/registry-maps.ts` — the `BLOCK_REGISTRY` config map and `BLOCK_META_REGISTRY` catalog-meta map (alphabetically). `blocks/registry.ts` holds only the accessor functions (`getBlock`, `getAllBlocks`, …).\n- **`tools.config.tool` runs during serialization (before variable resolution)** — never do `Number()` or other type coercions there, or dynamic references like `<Block.output>` are destroyed. Put all type coercions in `tools.config.params`, which runs during execution after variables resolve.\n\nFor the full authoring instructions — SubBlock property tables, `condition`/`dependsOn`/`required`/`mode`/`canonicalParamId` syntax, required block metadata (`integrationType`, `tags`, `authMode`, `docsLink`, `{Service}BlockMeta`), file-input/`normalizeFileInput` patterns, and checklists — use the skills: `/add-integration` (end-to-end), `/add-tools`, `/add-block`, `/add-trigger`.\n\n## Tables\n\nTable 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<ColumnType, …>` on `registry.ts` and `registry.server.ts` is a compile-time completeness gate: adding a type to the union errors until both entries exist.\n\nNever add a `case 'sometype':` outside `column-types/` — 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.\n","AGENTS.md":"# Sim Development Guidelines\n\nYou are a professional software engineer. All code must follow best practices: accurate, readable, clean, and efficient.\n\n## Global Standards\n\n- **Linting / Audit**: `bun run check:api-validation` must pass on PRs. Do not introduce route-local boundary Zod schemas, direct route Zod imports, or ad-hoc client wire types — see \"API Contracts\" and \"API Route Pattern\" below\n- **Logging**: Import `createLogger` from `@sim/logger`. Use `logger.info`, `logger.warn`, `logger.error` instead of `console.log`. Inside API routes wrapped with `withRouteHandler`, loggers automatically include the request ID — no manual `withMetadata({ requestId })` needed\n- **API Route Handlers**: All API route handlers (`GET`, `POST`, `PUT`, `DELETE`, `PATCH`) must run inside `withRouteHandler`. Ordinary internal and v2 handlers use the shared JSON/binary route builders, which already apply it; never double-wrap a builder. Use raw `withRouteHandler` only for documented protocol or lifecycle exceptions. See \"API Route Pattern\" below\n- **Comments**: Use TSDoc for documentation. No `====` separators. No non-TSDoc comments\n- **Styling**: Never update global styles. Keep all styling local to components\n- **ID Generation**: Never use `crypto.randomUUID()`, `nanoid`, or `uuid` package. Use `generateId()` (UUID v4) or `generateShortId()` (compact) from `@sim/utils/id`\n- **Common Utilities**: Use shared helpers from `@sim/utils` instead of inline implementations:\n  - `sleep(ms)` from `@sim/utils/helpers` — never `new Promise(resolve => setTimeout(resolve, ms))`\n  - `toError(e)` from `@sim/utils/errors` — normalize caught values to `Error`\n  - `getErrorMessage(e, fallback?)` from `@sim/utils/errors` — extract message string from unknown caught value; never write `e instanceof Error ? e.message : 'fallback'`\n  - `structuredClone(value)` — built-in deep clone; never `JSON.parse(JSON.stringify(...))`\n  - `omit(obj, keys)` / `filterUndefined(obj)` from `@sim/utils/object` — object trimming; never `Object.fromEntries(Object.entries(...).filter(...))`\n  - `truncate(str, maxLength, suffix?)` from `@sim/utils/string` — never inline slice + ellipsis\n  - `backoffWithJitter(attempt, retryAfterMs, options?)` / `parseRetryAfter(header)` from `@sim/utils/retry` — shared retry pacing; never reimplement exponential backoff inline\n- **Package Manager**: Use `bun` and `bunx`, not `npm` and `npx`\n\n## Architecture\n\n### Core Principles\n\n1. Single Responsibility: Each component, hook, store has one clear purpose\n2. Composition Over Complexity: Break down complex logic into smaller pieces\n3. Type Safety First: TypeScript interfaces for all props, state, return types\n4. Predictable State: Zustand for global state, useState for UI-only concerns\n\n### Application Operation Boundary\n\n- Every protected read, write, canonical resource lookup, or authorization-sensitive reference resolution enters through an authorized application use case.\n- 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.\n- Surface adapters authenticate and construct a `Principal`, apply request-rate policy, parse contracts, map input, and present results. They never query protected data, decide resource authorization, implement business transactions, or record semantic audit.\n- 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.\n- Copilot is a surface adapter. Use `createCopilotApplicationAdapter` and the domain's registered operation object; do not create Copilot-only authorization or business implementations.\n- Protected compound mutations belong in one top-level semantic application operation. Do not sequence independently committing mutations in a route or tool adapter.\n- 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.\n- Use the `migrate-application-operation` skill whenever creating or migrating a protected endpoint, tool command, or resource method.\n\n### Root Structure\n\n```\napps/\n├── sim/                    # Next.js app (UI + API routes + workflow editor)\n│   ├── app/                # Next.js app router (pages, API routes)\n│   ├── blocks/             # Block definitions and registry\n│   ├── components/         # Shared UI (emcn/, ui/)\n│   ├── executor/           # Workflow execution engine\n│   ├── hooks/              # Shared hooks (queries/, selectors/)\n│   ├── lib/                # App-wide utilities\n│   ├── providers/          # LLM provider integrations\n│   ├── stores/             # Zustand stores\n│   ├── tools/              # Tool definitions\n│   └── triggers/           # Trigger definitions\n└── realtime/               # Bun Socket.IO server (collaborative canvas)\n\npackages/\n├── audit/                  # @sim/audit\n├── auth/                   # @sim/auth — shared Better Auth verifier\n├── db/                     # @sim/db — drizzle schema + client\n├── logger/                 # @sim/logger\n├── platform-authz/         # @sim/platform-authz — workspace + workflow authz (subpath exports)\n├── realtime-protocol/      # @sim/realtime-protocol — socket op constants + zod schemas\n├── security/               # @sim/security — safeCompare\n├── tsconfig/               # shared tsconfig presets\n├── utils/                  # @sim/utils\n├── workflow-persistence/   # @sim/workflow-persistence\n└── workflow-types/         # @sim/workflow-types — pure BlockState/Loop/Parallel types\n```\n\n### Package boundaries\n\n- `apps/* → packages/*` only. Packages never import from `apps/*`.\n- `apps/realtime` intentionally avoids Next.js, React, the block/tool registry, provider SDKs, and the executor. Do not add imports from `@/lib/webhooks/providers/*`, `@/executor/*`, `@/blocks/*`, or `@/tools/*` to any package consumed by `apps/realtime`. CI enforces this via `scripts/check-monorepo-boundaries.ts` and `scripts/check-realtime-prune-graph.ts`.\n- Auth is shared across both apps via the Better Auth \"Shared Database Session\" pattern (same `BETTER_AUTH_SECRET`, same DB via `@sim/db`).\n\n### Naming Conventions\n\n- Components: PascalCase (`WorkflowList`)\n- Hooks: `use` prefix (`useWorkflowOperations`)\n- Files: kebab-case (`workflow-list.tsx`)\n- Stores: `stores/feature/store.ts`\n- Constants: SCREAMING_SNAKE_CASE\n- Interfaces: PascalCase with suffix (`WorkflowListProps`)\n\n## Imports\n\n**Always use absolute imports.** Never use relative imports.\n\n```typescript\n// ✓ Good\nimport { useWorkflowStore } from '@/stores/workflows/store'\n\n// ✗ Bad\nimport { useWorkflowStore } from '../../../stores/workflows/store'\n```\n\nUse barrel exports (`index.ts`) when a folder has 3+ exports. Do not re-export from non-barrel files; import directly from the source.\n\n### Import Order\n\n1. React/core libraries\n2. External libraries\n3. UI components (`@/components/emcn`, `@/components/ui`)\n4. Utilities (`@/lib/...`)\n5. Stores (`@/stores/...`)\n6. Feature imports\n7. CSS imports\n\nUse `import type { X }` for type-only imports.\n\n## TypeScript\n\n1. No `any` - Use proper types or `unknown` with type guards\n2. Always define props interface for components\n3. `as const` for constant objects/arrays\n4. Explicit ref types: `useRef<HTMLDivElement>(null)`\n\n## Components\n\n```typescript\n'use client' // Only if using hooks\n\nconst CONFIG = { SPACING: 8 } as const\n\ninterface ComponentProps {\n  requiredProp: string\n  optionalProp?: boolean\n}\n\nexport function Component({ requiredProp, optionalProp = false }: ComponentProps) {\n  // Order: refs → external hooks → store hooks → custom hooks → state → useMemo → useCallback → useEffect → return\n}\n```\n\nExtract when: 50+ lines, used in 2+ files, or has own state/logic. Keep inline when: < 10 lines, single use, purely presentational.\n\n## API Contracts\n\nBoundary HTTP request and response shapes for all routes under `apps/sim/app/api/**` live in `apps/sim/lib/api/contracts/**` (one file per resource family — `folders.ts`, `chats.ts`, `knowledge.ts`, etc.). Routes never define route-local boundary Zod schemas, and clients never define ad-hoc wire types — both sides consume the same contract.\n\n- Each contract is built with `defineRouteContract({ method, path, params?, query?, body?, headers?, response: { mode: 'json', schema } })` from `@/lib/api/contracts`\n- Contracts export named schemas (e.g., `createFolderBodySchema`) AND named TypeScript type aliases (e.g., `export type CreateFolderBody = z.input<typeof createFolderBodySchema>`)\n- Clients (hooks, utilities, components) import the named type aliases from the contract file. They must never write `z.input<...>` / `z.output<...>` themselves\n- Shared identifier schemas live in `apps/sim/lib/api/contracts/primitives.ts` (e.g., `workspaceIdSchema`, `workflowIdSchema`). Reuse these instead of redefining string-based ID schemas\n- Audit script: `bun run check:api-validation` enforces boundary policy and prints ratchet metrics for route Zod imports, route-local schema constructors, route `ZodError` references, client hook Zod imports, and related counters. It must pass on PRs. `bun run check:api-validation:strict` is the strict CI gate and additionally fails on annotations with empty reasons\n\nDomain validators that are not HTTP boundaries — tools, blocks, triggers, connectors, realtime handlers, and internal helpers — may still use Zod directly. The contract rule is boundary-only.\n\n### Boundary annotations\n\nA small number of legitimate exceptions to the boundary rules are tolerated when annotated. The audit script recognizes four annotation forms:\n\n- `// boundary-raw-fetch: <reason>` — placed on the line directly above a raw `fetch(` call in client hooks (`apps/sim/hooks/queries/**`, `apps/sim/hooks/selectors/**`) AND any same-origin `/api/...` fetch elsewhere under `apps/sim/**` outside an API route handler. Use only for documented exceptions: streaming responses, binary downloads, multipart uploads, signed-URL flows, OAuth redirects, and external-origin requests\n- `// double-cast-allowed: <reason>` — placed on the line directly above an `as unknown as X` cast outside test files\n- `// boundary-raw-json: <reason>` — placed on the line directly above a raw `await request.json()` / `await req.json()` read in a route handler. Use only when the body is a JSON-RPC envelope, a tolerant `.catch(() => ({}))` parse, or otherwise cannot go through `parseRequest`\n- `// untyped-response: <reason>` — placed on the line directly above a `schema: z.unknown()` response declaration in a contract file. Use only when the response body is genuinely opaque (user-supplied data, third-party passthrough)\n\nPlacement rule: the annotation must immediately precede the call or cast. Up to three non-empty preceding comment lines are tolerated, so additional context comments above the annotation are fine. The reason must be non-empty after trimming — annotations with empty reasons fail strict mode (`annotationsMissingReason`).\n\nWhole-file allowlists for routes (legitimate non-boundary or auth-handled routes that legitimately import Zod for non-boundary reasons) go through `INDIRECT_ZOD_ROUTES` in `scripts/check-api-validation-contracts.ts`, not per-line annotations.\n\nExamples:\n\n```ts\n// boundary-raw-fetch: streaming SSE chunks must be processed as they arrive\nconst response = await fetch(`/api/copilot/chat/stream?chatId=${chatId}`, { signal })\n```\n\n```ts\n// double-cast-allowed: legacy provider type lacks the discriminator field we need\nconst provider = config as unknown as LegacyProvider\n```\n\n## API Route Pattern\n\nEvery route method must run inside `withRouteHandler`. Ordinary internal and v2 JSON/binary routes use `defineInternalJsonRoute`, `defineV2JsonRoute`, or the matching binary/stream builder. These builders already apply `withRouteHandler`; never wrap them again. Use raw `withRouteHandler` only for explicit protocol or lifecycle exceptions such as streaming, multipart control, large-body admission, OAuth, or public execution.\n\nRoutes never `import { z } from 'zod'` and never define route-local boundary schemas. Declarative builders consume contracts and own authentication, admission, parsing, use-case execution, response validation, and error projection. A raw special route consumes the same contracts and validates with canonical helpers from `@/lib/api/server`, after authentication and cheap admission:\n\n- `parseRequest(contract, request, context, options?)` — fully contract-bound routes; parses params, query, body, and headers in one call. Pass `{}` for `context` on routes without route params, or the route's `context` argument when route params exist. Returns a discriminated union; check `parsed.success` and return `parsed.response` on failure\n- `validationErrorResponse(error)` and `getValidationErrorMessage(error, fallback)` — produce 400 responses from a `ZodError`\n- `validationErrorResponseFromError(error)` — when handling unknown caught errors that may or may not be a `ZodError`\n- `isZodError(error)` — type guard. Routes never use `instanceof z.ZodError`\n\n### Ordinary authorized JSON route\n\n```typescript\nexport const PATCH = defineInternalJsonRoute({\n  contract: renameWidgetContract,\n  auth: internalSessionAuth,\n  operation: widgetOperations.rename,\n  rateLimit: internalRateLimits.none({ reason: 'Preserve existing internal behavior' }),\n  errorPolicy: internalWidgetErrorPolicy,\n  mapInput: ({ params, body }) => ({\n    widgetId: params.widgetId,\n    assertedWorkspaceId: params.workspaceId,\n    name: body.name,\n  }),\n  useCase: renameWidget,\n  present: ({ widget }) => ({ success: true, widget }),\n})\n```\n\nThe contract, operation, and use case must agree at definition time. Authentication and request-rate admission happen before parsing; canonical loading and authorization happen in the application use case. The presenter returns only the surface success body.\n\nRoutes under `apps/sim/app/api/v1/**` use the shared middleware in `apps/sim/app/api/v1/middleware.ts` for auth, rate-limit, and workspace access. Compose contract validation inside that middleware — never reimplement auth/rate-limit per-route.\n\nNever export a bare `async function GET/POST/...`. Export the result of a shared builder or, for a documented special route, `withRouteHandler(...)`.\n\n### Adding a new boundary feature end-to-end\n\nWhen adding a new route + client surface, follow this order. Each step has one place it lives.\n\n1. **Author the contract first** in `apps/sim/lib/api/contracts/<domain>.ts` (or a subdirectory for large domains: `knowledge/`, `selectors/`, `tools/`). Define one schema per request slice (`params`, `query`, `body`, `headers`) and one for the response, then wrap with `defineRouteContract`. Export named type aliases (`z.input` for inputs, `z.output` for outputs).\n2. **Define the semantic operation and application use case** under `apps/sim/lib/<domain>/application/`. The use case owns canonical loading, asserted-scope checks, current authorization, business behavior, semantic audit, and shared domain effects.\n3. **Implement the route adapter** in `apps/sim/app/api/<path>/route.ts` with the appropriate shared builder. Declare auth, operation, rate policy, error policy, input mapping, use case, and presenter. Auth always runs **before** parsing. Use raw `withRouteHandler` only for an explicit special route, and keep protected work in application use cases.\n4. **Add the React Query hook** in `apps/sim/hooks/queries/<domain>.ts`. Use `requestJson(contract, input)` for the call. Build a hierarchical query-key factory (`all` → `lists()` → `list(workspaceId)` → `details()` → `detail(id)`) so invalidations can target prefixes.\n5. **Use the hook in the component**. The mutation's `data` and `error` are fully typed from the contract; surface `error.message` (already extracted from the response body's `error` or `message` field by `requestJson`).\n\n### Schema review checklist (read the contract diff like a DB migration)\n\nLLMs will write contracts that compile but are sloppy. The human reviewer should optimize attention on:\n\n- **`required` vs `optional` vs `nullable` is correct**. `optional()` allows omission; `nullable()` allows `null`; chaining both creates a tri-state that's almost never what you want.\n- **Response schema matches the route's actual JSON output**. The most common drift bug — route emits a field the schema doesn't declare, or omits a required field. Walk every `NextResponse.json(...)` callsite against the schema.\n- **Error messages are descriptive**. `'fileName cannot be empty'` beats `'Required'`. Use the second arg of `min(1, '...')`, `nonempty('...')`, etc. For cross-field refines, use `superRefine` with a `path` and a message that names the failing field.\n- **Bounds are set** on arrays (`.min(1)`, `.max(N)`), strings (`.min(1).max(N)` for IDs/names), and numbers (`.min().max()` for limits/sizes).\n- **`z.unknown()` is a smell** unless the data is genuinely arbitrary (provider passthrough, user-defined tool result, JSON-RPC envelope). When kept, must be annotated `// untyped-response: <specific reason>` in a `schema:` slot.\n- **Discriminated unions over plain unions** when the wire has a discriminant field — gives clients exhaustive narrowing.\n\nCI (`bun run check:api-validation:strict`) catches structural violations (Zod imports in routes, raw `request.json()`, double casts, missing annotations). It does **not** catch these schema-quality judgments — that's the human's job in PR review.\n\n## Hooks\n\n```typescript\ninterface UseFeatureProps { id: string }\n\nexport function useFeature({ id }: UseFeatureProps) {\n  const idRef = useRef(id)\n  const [data, setData] = useState<Data | null>(null)\n  \n  useEffect(() => { idRef.current = id }, [id])\n  \n  const fetchData = useCallback(async () => { ... }, []) // Empty deps when using refs\n  \n  return { data, fetchData }\n}\n```\n\n## Zustand Stores\n\nStores live in `stores/`. Complex stores split into `store.ts` + `types.ts`.\n\n```typescript\nimport { create } from 'zustand'\nimport { devtools } from 'zustand/middleware'\n\nconst initialState = { items: [] as Item[] }\n\nexport const useFeatureStore = create<FeatureState>()(\n  devtools(\n    (set, get) => ({\n      ...initialState,\n      setItems: (items) => set({ items }),\n      reset: () => set(initialState),\n    }),\n    { name: 'feature-store' }\n  )\n)\n```\n\nUse `devtools` middleware. Use `persist` only when data should survive reload with `partialize` to persist only necessary state.\n\n## React Query\n\nAll React Query hooks live in `hooks/queries/`. All server state must go through React Query — never use `useState` + `fetch` in components for data fetching or mutations.\n\n### Client Boundary\n\nHooks consume contracts the same way routes do. Every same-origin JSON call must go through `requestJson(contract, ...)` from `@/lib/api/client/request` instead of raw `fetch`:\n\n- Hooks import named type aliases from `@/lib/api/contracts/**`. Never write `z.input<...>` / `z.output<...>` in hooks, and never `import { z } from 'zod'` in client code\n- `requestJson` parses params, query, body, and headers against the contract on the way out and validates the JSON response on the way back. Hooks always forward `signal` for cancellation\n- Documented exceptions for raw `fetch`: streaming responses, binary downloads, multipart uploads, signed-URL flows, OAuth redirects, and external-origin requests. Mark each raw `fetch` with a TSDoc comment explaining which exception applies. The `// boundary-raw-fetch` annotation is required not only in client hooks but for any same-origin `/api/...` fetch anywhere under `apps/sim/**` outside an API route handler — strict CI flags these regardless of location\n\n```typescript\nimport { keepPreviousData, useQuery } from '@tanstack/react-query'\nimport { requestJson } from '@/lib/api/client/request'\nimport { listEntitiesContract, type EntityList } from '@/lib/api/contracts/entities'\n\nasync function fetchEntities(workspaceId: string, signal?: AbortSignal): Promise<EntityList> {\n  const data = await requestJson(listEntitiesContract, {\n    query: { workspaceId },\n    signal,\n  })\n  return data.entities\n}\n\nexport function useEntityList(workspaceId?: string) {\n  return useQuery({\n    queryKey: entityKeys.list(workspaceId),\n    queryFn: ({ signal }) => fetchEntities(workspaceId as string, signal),\n    enabled: Boolean(workspaceId),\n    staleTime: 60 * 1000,\n    placeholderData: keepPreviousData,\n  })\n}\n```\n\n### Query Key Factory\n\nEvery file must have a hierarchical key factory with an `all` root key and intermediate plural keys for prefix invalidation:\n\n```typescript\nexport const entityKeys = {\n  all: ['entity'] as const,\n  lists: () => [...entityKeys.all, 'list'] as const,\n  list: (workspaceId?: string) => [...entityKeys.lists(), workspaceId ?? ''] as const,\n  details: () => [...entityKeys.all, 'detail'] as const,\n  detail: (id?: string) => [...entityKeys.details(), id ?? ''] as const,\n}\n```\n\n### Query Hooks\n\n- Every `queryFn` must forward `signal` for request cancellation\n- Every query must have an explicit `staleTime`\n- Use `keepPreviousData` only on variable-key queries (where params change), never on static keys\n\n```typescript\nexport function useEntityList(workspaceId?: string) {\n  return useQuery({\n    queryKey: entityKeys.list(workspaceId),\n    queryFn: ({ signal }) => fetchEntities(workspaceId as string, signal),\n    enabled: Boolean(workspaceId),\n    staleTime: 60 * 1000,\n    placeholderData: keepPreviousData, // OK: workspaceId varies\n  })\n}\n```\n\n### Mutation Hooks\n\n- Use targeted invalidation (`entityKeys.lists()`) not broad (`entityKeys.all`) when possible\n- For optimistic updates: use `onSettled` (not `onSuccess`) for cache reconciliation — `onSettled` fires on both success and error\n- Don't include mutation objects in `useCallback` deps — `.mutate()` is stable in TanStack Query v5\n\n```typescript\nexport function useUpdateEntity() {\n  const queryClient = useQueryClient()\n  return useMutation({\n    mutationFn: async (variables) => { /* ... */ },\n    onMutate: async (variables) => {\n      await queryClient.cancelQueries({ queryKey: entityKeys.detail(variables.id) })\n      const previous = queryClient.getQueryData(entityKeys.detail(variables.id))\n      queryClient.setQueryData(entityKeys.detail(variables.id), /* optimistic */)\n      return { previous }\n    },\n    onError: (_err, variables, context) => {\n      queryClient.setQueryData(entityKeys.detail(variables.id), context?.previous)\n    },\n    onSettled: (_data, _error, variables) => {\n      queryClient.invalidateQueries({ queryKey: entityKeys.lists() })\n      queryClient.invalidateQueries({ queryKey: entityKeys.detail(variables.id) })\n    },\n  })\n}\n```\n\n## Styling\n\nUse Tailwind only, no inline styles. Use `cn()` from `@/lib/core/utils/cn` for conditional classes.\n\n```typescript\n<div className={cn('base-classes', isActive && 'active-classes')} />\n```\n\nFor equal height and width, use the `size-*` shorthand — never `h-[Npx] w-[Npx]` or `h-N w-N`. Default icon size is `size-[14px]`.\n\n```typescript\n<Icon className='size-[14px] text-[var(--text-icon)]' />\n```\n\nOn chip components (see \"EMCN Components\"), drive chrome through PROPS, not `className`: `error` for the error state, `icon`/`endAdornment` for adornments, `inputClassName` for the inner field. `className` carries ONLY layout/sizing — never re-specify canonical chrome (border, fill, radius, height, text/icon color) or add focus rings. Full consumer rules in `.claude/rules/sim-styling.md`.\n\n## EMCN Components\n\nImport from `@/components/emcn`, never from subpaths (except CSS files). Use CVA only when 2+ genuine variants exist; otherwise plain `cn()`.\n\nThe chip family is the canonical UI chrome and is progressively replacing the legacy EMCN primitives — always reach for the chip equivalent: `ChipInput` over `Input`, `ChipTextarea` over `Textarea`, `ChipModal`/`ChipModalField` over `Modal`, `ChipSelect`/`ChipCombobox` (searchable) or `ChipDropdown` (simple menu-select) over `Select`/`Combobox`, `ChipSwitch` over `Switch`, `ChipDatePicker` over a raw date field, `Chip`/`ChipLink` for pill buttons/links, `ChipTag` for inline tags/badges. For context/action menus the canonical control is `DropdownMenu` (not a chip, but the standard menu — not a hand-rolled popover). Components OWN their chrome (single source of truth) — consumers pass props, not class overrides. Authoring rules in `.claude/rules/emcn-components.md`; consumer rules in `.claude/rules/sim-styling.md`.\n\nInside a `ChipModalBody`, EVERY labeled field MUST be a `ChipModalField` — never hand-roll a field row (a raw `<div>` + a hand-rolled `<p>`/`<label>` title + a bare `ChipInput`/`ChipTextarea`). `ChipModalBody` applies `px-2` + `gap-4`; `ChipModalField` adds ANOTHER `px-2`, so each field lands at effective `px-4`, exactly matching `ChipModalHeader`/`ChipModalFooter` (`px-4`). Hand-rolled rows skip the field's gutter and sit at `px-2`, visibly misaligned with the header/footer. For controls `ChipModalField` does not cover (`ChipCombobox`, `ChipSelect`, `DatePicker`, `TimePicker`, `ButtonGroup`, arbitrary JSX), use `ChipModalField type='custom'` with a `title` — it still applies the `px-2` gutter and renders the canonical `Label`. Drive intent via props (`title`/`value`/`onChange`/`error`/`hint`/`required`/`flush`); never pass `variant`/`className`/`id` to the inner control, and never add a body-level wrapper `<div>` with a custom `gap-*` that fights `ChipModalBody`'s `gap-4`.\n\n## Design-System Consolidation\n\nPrinciples when building or migrating shared UI:\n\n- One canonical source of truth for shared chrome — compose it, never re-derive it per consumer.\n- Props-driven API over `className` overrides — reaching for `className` to change chrome is a smell; expose a prop instead.\n- Discriminated-union props for modes (e.g. `ChipDropdown multiple`) over near-duplicate components.\n- Delete legacy variants/components after migration — no parallel paths left behind.\n- Plain `cn()` for a single error/state toggle; CVA only for genuinely multiple variants.\n- Align consumers to the canonical defaults — normal weight, `--text-body` text, `--text-icon` icons.\n- Verify referenced CSS vars exist — an undefined var silently falls back to `currentColor` (black-bug).\n\n## Testing\n\nUse Vitest. Test files: `feature.ts` → `feature.test.ts`. See `.cursor/rules/sim-testing.mdc` for full details.\n\n### Global Mocks (vitest.setup.ts)\n\n`@sim/db`, `@sim/db/schema`, `drizzle-orm`, `@sim/logger`, `@sim/platform-authz/workflow`, `@/blocks/registry`, `@/lib/auth`, `@/lib/auth/hybrid`, `@/lib/core/utils/request`, `@trigger.dev/sdk`, and store mocks are provided globally. Do NOT re-mock them unless overriding behavior. (The `vi.mock('@/lib/auth', ...)` in the example below is an override of the global mock so `getSession` can be controlled per-test.)\n\n### Standard Test Pattern\n\n```typescript\n/**\n * @vitest-environment node\n */\nimport { createMockRequest } from '@sim/testing'\nimport { beforeEach, describe, expect, it, vi } from 'vitest'\n\nconst { mockGetSession } = vi.hoisted(() => ({\n  mockGetSession: vi.fn(),\n}))\n\nvi.mock('@/lib/auth', () => ({\n  auth: { api: { getSession: vi.fn() } },\n  getSession: mockGetSession,\n}))\n\nimport { GET } from '@/app/api/my-route/route'\n\ndescribe('my route', () => {\n  beforeEach(() => {\n    vi.clearAllMocks()\n    mockGetSession.mockResolvedValue({ user: { id: 'user-1' } })\n  })\n  it('returns data', async () => { ... })\n})\n```\n\n### Performance Rules\n\n- **NEVER** use `vi.resetModules()` + `vi.doMock()` + `await import()` — use `vi.hoisted()` + `vi.mock()` + static imports\n- **NEVER** use `vi.importActual()` — mock everything explicitly\n- **NEVER** use `mockAuth()`, `mockConsoleLogger()`, `setupCommonApiMocks()` from `@sim/testing` — they use `vi.doMock()` internally\n- **Mock heavy deps** (`@/blocks`, `@/tools/registry`, `@/triggers`) in tests that don't need them\n- **Use `@vitest-environment node`** unless DOM APIs are needed (`window`, `document`, `FormData`)\n- **Avoid real timers** — use 1ms delays or `vi.useFakeTimers()`\n\nUse `@sim/testing` mocks/factories over local test data.\n\n## Utils Rules\n\n- Never create `utils.ts` for single consumer - inline it\n- Create `utils.ts` when 2+ files need the same helper\n- Check existing sources in `lib/` before duplicating\n\n## Adding Integrations\n\nNew integrations are built in order: **Tools** → **Block** → **Icon** → (optional) **Trigger**. Always look up the service's API docs first.\n\nTwo hard rules that the skills assume:\n\n- **Tool IDs are `snake_case`** (`service_action`) and must be registered in `tools/registry.ts`; blocks register in `blocks/registry.ts` (alphabetically).\n- **`tools.config.tool` runs during serialization (before variable resolution)** — never do `Number()` or other type coercions there, or dynamic references like `<Block.output>` are destroyed. Put all type coercions in `tools.config.params`, which runs during execution after variables resolve.\n\nFor the full authoring instructions — SubBlock property tables, `condition`/`dependsOn`/`required`/`mode`/`canonicalParamId` syntax, required block metadata (`integrationType`, `tags`, `authMode`, `docsLink`, `{Service}BlockMeta`), file-input/`normalizeFileInput` patterns, and checklists — use the skills: `/add-integration` (end-to-end), `/add-tools`, `/add-block`, `/add-trigger`.\n\n## Tables\n\nTable 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<ColumnType, …>` on `registry.ts` and `registry.server.ts` is a compile-time completeness gate: adding a type to the union errors until both entries exist.\n\nNever add a `case 'sometype':` outside `column-types/` — 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.\n"}}