agentleFS
Ame-AlAme-Al/ame/CLAUDE.md

ame

CLAUDE.md1 starsChanged 2 months agoApache-2.0View on GitHub

You are a professional software engineer. All code must follow best practices: accurate, readable, clean, and efficient.

We split this CLAUDE.md into the rules it gives an agent

38 rules. About 0 are common, 8 are distinctive, and 30 are very specific to this project.

  • ≈0

    Common · estimate

    Many files like this have a rule of this kind.

  • ≈8

    Distinctive · estimate

    This author's own choices. Worth a look.

  • 30

    Very specific

    Tied to their project's names, files or tools. Probably skip.

  • Common
  • Distinctive
  • Very specific
  1. Very specific:

    Logging: Import createLogger from @sim/logger.

    line 8
  2. Very specific:

    Comments: Use TSDoc for documentation.

    line 10
  3. Distinctive:

    Styling: Never update global styles.

    line 11
  4. Very specific:

    ID Generation: Never use crypto.randomUUID(), nanoid, or uuid package.

    line 12
  5. Very specific:

    Common Utilities: Use shared helpers from @sim/utils instead of inline implementations:

    line 13
  6. Very specific:

    Package Manager: Use bun and bunx, not npm and npx

    line 21
  7. Distinctive:

    Every protected read, write, canonical resource lookup, or authorization-sensitive reference resolution enters through an authorized …

    line 35
  8. Distinctive:

    Never substitute a billing owner, uploader, creator, or API-key owner for the acting principal.

    line 41
  9. Very specific:

    Use the migrate-application-operation skill whenever creating or migrating a protected endpoint, tool command, or resource method.

    line 42
  10. Distinctive:

    Always use absolute imports: Never use relative imports.

    line 92
  11. Very specific:

    Use barrel exports (index.ts) when a folder has 3+ exports.

    line 102
  12. Very specific:

    Use import type { X } for type-only imports.

    line 114
  13. Distinctive:

    Always define props interface for components

    line 119
  14. Very specific:

    Never export a bare async function GET/POST/....

    line 215
  15. Distinctive:

    When adding a new route + client surface, follow this order.

    line 219
  16. Distinctive:

    Discriminated unions over plain unions: when the wire has a discriminant field — gives clients exhaustive narrowing.

    line 236
  17. Very specific:

    Use devtools middleware. Use persist only when data should survive reload with partialize to persist only necessary state.

    line 279
  18. Very specific:

    All React Query hooks live in hooks/queries/.

    line 283
  19. Very specific:

    Every file must have a hierarchical key factory with an all root key and intermediate plural keys for prefix invalidation:

    line 321
  20. Very specific:

    Every queryFn must forward signal for request cancellation

    line 335
  21. Very specific:

    Every query must have an explicit staleTime, assigned from a named exported constant, never an inline numeric literal — a server-side …

    line 336
  22. Very specific:

    Use keepPreviousData only on variable-key queries (where params change), never on static keys

    line 337
  23. Very specific:

    Use targeted invalidation (entityKeys.lists()) not broad (entityKeys.all) when possible

    line 353
  24. Very specific:

    Don't include mutation objects in useCallback deps — .mutate() is stable in TanStack Query v5

    line 355
  25. Very specific:

    Use Tailwind only, no inline styles.

    line 393
  26. Very specific:

    Import components, cn, and tokens from the @sim/emcn barrel.

    line 409
  27. Distinctive:

    Delete legacy variants/components after migration — no parallel paths left behind.

    line 422
  28. Very specific:

    Verify referenced CSS vars exist — an undefined var silently falls back to currentColor (black-bug).

    line 425
  29. Very specific:

    Use Vitest. Test files: feature.ts → feature.test.ts.

    line 429
  30. Very specific:

    NEVER: use vi.resetModules() + vi.doMock() + await import() — use vi.hoisted() + vi.mock() + static imports

    line 466
  31. Very specific:

    NEVER: use vi.importActual() — mock everything explicitly

    line 467
  32. Very specific:

    NEVER: use mockAuth(), mockConsoleLogger(), setupCommonApiMocks() from @sim/testing — they use vi.doMock() internally

    line 468
  33. Very specific:

    Use @sim/testing mocks/factories over local test data.

    line 473
  34. Very specific:

    Never hand-roll TTL arithmetic — a Map plus Date.now() - fetchedAt < TTL re-implements expiry, the ceiling, and coalescing badly.

    line 477
  35. Very specific:

    Never create utils.ts for single consumer - inline it

    line 493
  36. Very specific:

    Create utils.ts when 2+ files need the same helper

    line 494
  37. Very specific:

    Check existing sources in lib/ before duplicating

    line 495
  38. Very specific:

    Never add a case 'sometype': outside column-types/ — a missing arm fails silently (a wrong jsonbCast breaks every filter on the column).

    line 512

≈ “Common” is an estimate: at least 1 in 5 of the 25,680 CLAUDE.md files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.

Keep what fits and drop the rest. Your version, ready for your agents.
Files
CLAUDE.md

Sim Development Guidelines

You are a professional software engineer. All code must follow best practices: accurate, readable, clean, and efficient.

Global Standards

  • 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
  • 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
  • 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
  • Comments: Use TSDoc for documentation. No ==== separators. No non-TSDoc comments
  • Styling: Never update global styles. Keep all styling local to components
  • ID Generation: Never use crypto.randomUUID(), nanoid, or uuid package. Use generateId() (UUID v4) or generateShortId() (compact) from @sim/utils/id
  • Common Utilities: Use shared helpers from @sim/utils instead of inline implementations:
    • sleep(ms) from @sim/utils/helpers — never new Promise(resolve => setTimeout(resolve, ms))
    • toError(e) from @sim/utils/errors — normalize caught values to Error
    • getErrorMessage(e, fallback?) from @sim/utils/errors — extract message string from unknown caught value; never write 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 — object trimming; never Object.fromEntries(Object.entries(...).filter(...))
    • truncate(str, maxLength, suffix?) from @sim/utils/string — never inline slice + ellipsis
    • backoffWithJitter(attempt, retryAfterMs, options?) / parseRetryAfter(header) from @sim/utils/retry — shared retry pacing; never reimplement exponential backoff inline
  • Package Manager: Use bun and bunx, not npm and npx
  • 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

Architecture

Core Principles

  1. Single Responsibility: Each component, hook, store has one clear purpose
  2. Composition Over Complexity: Break down complex logic into smaller pieces
  3. Type Safety First: TypeScript interfaces for all props, state, return types
  4. Predictable State: Zustand for global state, useState for UI-only concerns

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 results. They never query protected data, decide resource authorization, implement business transactions, or record semantic audit.
  • 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.
  • Copilot is a surface adapter. Use createCopilotApplicationAdapter and the domain's registered operation object; do not create Copilot-only authorization or business implementations.
  • Protected compound mutations belong in one top-level semantic application operation. Do not sequence 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.

Root Structure

apps/
├── sim/                    # Next.js app (UI + API routes + workflow editor)
│   ├── app/                # Next.js app router (pages, API routes)
│   ├── blocks/             # Block definitions and registry
│   ├── components/         # Shared UI (emcn/, ui/)
│   ├── executor/           # Workflow execution engine
│   ├── hooks/              # Shared hooks (queries/, selectors/)
│   ├── lib/                # App-wide utilities
│   ├── providers/          # LLM provider integrations
│   ├── stores/             # Zustand stores
│   ├── tools/              # Tool definitions
│   └── triggers/           # Trigger definitions
└── realtime/               # Bun Socket.IO server (collaborative canvas)

packages/
├── audit/                  # @sim/audit
├── auth/                   # @sim/auth — shared Better Auth verifier
├── db/                     # @sim/db — drizzle schema + client
├── logger/                 # @sim/logger
├── platform-authz/         # @sim/platform-authz — workspace + workflow authz (subpath exports)
├── realtime-protocol/      # @sim/realtime-protocol — socket op constants + zod schemas
├── security/               # @sim/security — safeCompare
├── tsconfig/               # shared tsconfig presets
├── utils/                  # @sim/utils
├── workflow-persistence/   # @sim/workflow-persistence
└── workflow-types/         # @sim/workflow-types — pure BlockState/Loop/Parallel types

Package boundaries

  • apps/* → packages/* only. Packages never import from apps/*.
  • 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.
  • Auth is shared across both apps via the Better Auth "Shared Database Session" pattern (same BETTER_AUTH_SECRET, same DB via @sim/db).

Naming Conventions

  • Components: PascalCase (WorkflowList)
  • Hooks: use prefix (useWorkflowOperations)
  • Files: kebab-case (workflow-list.tsx)
  • Stores: stores/feature/store.ts
  • Constants: SCREAMING_SNAKE_CASE
  • Interfaces: PascalCase with suffix (WorkflowListProps)

Imports

Always use absolute imports. Never use relative imports.

// ✓ Good
import { useWorkflowStore } from '@/stores/workflows/store'

// ✗ Bad
import { useWorkflowStore } from '../../../stores/workflows/store'

Use barrel exports (index.ts) when a folder has 3+ exports. Do not re-export from non-barrel files; import directly from the source.

Import Order

  1. React/core libraries
  2. External libraries
  3. UI components (@sim/emcn, @/components/ui)
  4. Utilities (@/lib/...)
  5. Stores (@/stores/...)
  6. Feature imports
  7. CSS imports

Use import type { X } for type-only imports.

TypeScript

  1. No any - Use proper types or unknown with type guards
  2. Always define props interface for components
  3. as const for constant objects/arrays
  4. Explicit ref types: useRef<HTMLDivElement>(null)

Components

'use client' // Only if using hooks

const CONFIG = { SPACING: 8 } as const

interface ComponentProps {
  requiredProp: string
  optionalProp?: boolean
}

export function Component({ requiredProp, optionalProp = false }: ComponentProps) {
  // Order: refs → external hooks → store hooks → custom hooks → state → useMemo → useCallback → useEffect → return
}

Extract when: 50+ lines, used in 2+ files, or has own state/logic. Keep inline when: < 10 lines, single use, purely presentational.

Behavior-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.

API Contracts

Boundary 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.

  • Each contract is built with defineRouteContract({ method, path, params?, query?, body?, headers?, response: { mode: 'json', schema } }) from @/lib/api/contracts
  • Contracts export named schemas (e.g., createFolderBodySchema) AND named TypeScript type aliases (e.g., export type CreateFolderBody = z.input<typeof createFolderBodySchema>)
  • Clients (hooks, utilities, components) import the named type aliases from the contract file. They must never write z.input<...> / z.output<...> themselves
  • 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
  • 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

Domain 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.

Boundary annotations

A small number of legitimate exceptions to the boundary rules are tolerated when annotated. The audit script recognizes four annotation forms:

  • // 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
  • // double-cast-allowed: <reason> — placed on the line directly above an as unknown as X cast outside test files
  • // 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
  • // 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)

Placement 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).

Whole-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.

Examples:

// boundary-raw-fetch: streaming SSE chunks must be processed as they arrive
const response = await fetch(`/api/copilot/chat/stream?chatId=${chatId}`, { signal })
// double-cast-allowed: legacy provider type lacks the discriminator field we need
const provider = config as unknown as LegacyProvider

API Route Pattern

Every 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.

Routes 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:

  • 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
  • validationErrorResponse(error) and getValidationErrorMessage(error, fallback) — produce 400 responses from a ZodError
  • validationErrorResponseFromError(error) — when handling unknown caught errors that may or may not be a ZodError
  • isZodError(error) — type guard. Routes never use instanceof z.ZodError

Ordinary authorized JSON route

export const PATCH = defineInternalJsonRoute({
  contract: renameWidgetContract,
  auth: internalSessionAuth,
  operation: widgetOperations.rename,
  rateLimit: internalRateLimits.none({ reason: 'Preserve existing internal behavior' }),
  errorPolicy: internalWidgetErrorPolicy,
  mapInput: ({ params, body }) => ({
    widgetId: params.widgetId,
    assertedWorkspaceId: params.workspaceId,
    name: body.name,
  }),
  useCase: renameWidget,
  present: ({ widget }) => ({ success: true, widget }),
})

The 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.

Routes 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.

Never export a bare async function GET/POST/.... Export the result of a shared builder or, for a documented special route, withRouteHandler(...).

Adding a new boundary feature end-to-end

When adding a new route + client surface, follow this order. Each step has one place it lives.

  1. 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).
  2. 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.
  3. 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.
  4. 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.
  5. 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).

Schema review checklist (read the contract diff like a DB migration)

LLMs will write contracts that compile but are sloppy. The human reviewer should optimize attention on:

  • 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.
  • 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.
  • 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.
  • Bounds are set on arrays (.min(1), .max(N)), strings (.min(1).max(N) for IDs/names), and numbers (.min().max() for limits/sizes).
  • 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.
  • Discriminated unions over plain unions when the wire has a discriminant field — gives clients exhaustive narrowing.

CI (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.

Hooks

interface UseFeatureProps { id: string }

export function useFeature({ id }: UseFeatureProps) {
  const idRef = useRef(id)
  const [data, setData] = useState<Data | null>(null)
  
  useEffect(() => { idRef.current = id }, [id])
  
  const fetchData = useCallback(async () => { ... }, []) // Empty deps when using refs
  
  return { data, fetchData }
}

Zustand Stores

Stores live in stores/. Complex stores split into store.ts + types.ts.

import { create } from 'zustand'
import { devtools } from 'zustand/middleware'

const initialState = { items: [] as Item[] }

export const useFeatureStore = create<FeatureState>()(
  devtools(
    (set, get) => ({
      ...initialState,
      setItems: (items) => set({ items }),
      reset: () => set(initialState),
    }),
    { name: 'feature-store' }
  )
)

Use devtools middleware. Use persist only when data should survive reload with partialize to persist only necessary state.

React Query

All 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.

Client Boundary

Hooks 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:

  • 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
  • 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
  • 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
import { keepPreviousData, useQuery } from '@tanstack/react-query'
import { requestJson } from '@/lib/api/client/request'
import { listEntitiesContract, type EntityList } from '@/lib/api/contracts/entities'

export const ENTITY_LIST_STALE_TIME = 60 * 1000

async function fetchEntities(workspaceId: string, signal?: AbortSignal): Promise<EntityList> {
  const data = await requestJson(listEntitiesContract, {
    query: { workspaceId },
    signal,
  })
  return data.entities
}

export function useEntityList(workspaceId?: string) {
  return useQuery({
    queryKey: entityKeys.list(workspaceId),
    queryFn: ({ signal }) => fetchEntities(workspaceId as string, signal),
    enabled: Boolean(workspaceId),
    staleTime: ENTITY_LIST_STALE_TIME,
    placeholderData: keepPreviousData,
  })
}

Query Key Factory

Every file must have a hierarchical key factory with an all root key and intermediate plural keys for prefix invalidation:

export const entityKeys = {
  all: ['entity'] as const,
  lists: () => [...entityKeys.all, 'list'] as const,
  list: (workspaceId?: string) => [...entityKeys.lists(), workspaceId ?? ''] as const,
  details: () => [...entityKeys.all, 'detail'] as const,
  detail: (id?: string) => [...entityKeys.details(), id ?? ''] as const,
}

Query Hooks

  • Every queryFn must forward signal for request cancellation
  • 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
  • Use keepPreviousData only on variable-key queries (where params change), never on static keys
export function useEntityList(workspaceId?: string) {
  return useQuery({
    queryKey: entityKeys.list(workspaceId),
    queryFn: ({ signal }) => fetchEntities(workspaceId as string, signal),
    enabled: Boolean(workspaceId),
    staleTime: ENTITY_LIST_STALE_TIME,
    placeholderData: keepPreviousData, // OK: workspaceId varies
  })
}

Mutation Hooks

  • Use targeted invalidation (entityKeys.lists()) not broad (entityKeys.all) when possible
  • For optimistic updates: use onSettled (not onSuccess) for cache reconciliation — onSettled fires on both success and error
  • Don't include mutation objects in useCallback deps — .mutate() is stable in TanStack Query v5
export function useUpdateEntity() {
  const queryClient = useQueryClient()
  return useMutation({
    mutationFn: async (variables) => { /* ... */ },
    onMutate: async (variables) => {
      await queryClient.cancelQueries({ queryKey: entityKeys.detail(variables.id) })
      const previous = queryClient.getQueryData(entityKeys.detail(variables.id))
      queryClient.setQueryData(entityKeys.detail(variables.id), /* optimistic */)
      return { previous }
    },
    onError: (_err, variables, context) => {
      queryClient.setQueryData(entityKeys.detail(variables.id), context?.previous)
    },
    onSettled: (_data, _error, variables) => {
      queryClient.invalidateQueries({ queryKey: entityKeys.lists() })
      queryClient.invalidateQueries({ queryKey: entityKeys.detail(variables.id) })
    },
  })
}

URL / Query-Param State

Shareable 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 — 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).

Co-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.

List & Menu Ordering

A 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.

Encode 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.

Styling

Use Tailwind only, no inline styles. Use cn() from @sim/emcn for conditional classes.

<div className={cn('base-classes', isActive && 'active-classes')} />

For equal height and width, use the size-* shorthand — never h-[Npx] w-[Npx] or h-N w-N. Default icon size is size-[14px].

<Icon className='size-[14px] text-[var(--text-icon)]' />

On 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.

EMCN Components

Import components, cn, and tokens from the @sim/emcn barrel; icons come from the @sim/emcn/icons subpath, and CSS modules from their file path. Never deep-import other component subpaths. Use CVA only when 2+ genuine variants exist; otherwise plain cn().

The 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.

Inside 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.

Design-System Consolidation

Principles when building or migrating shared UI:

  • One canonical source of truth for shared chrome — compose it, never re-derive it per consumer.
  • Props-driven API over className overrides — reaching for className to change chrome is a smell; expose a prop instead.
  • Discriminated-union props for modes (e.g. ChipDropdown multiple) over near-duplicate components.
  • Delete legacy variants/components after migration — no parallel paths left behind.
  • Plain cn() for a single error/state toggle; CVA only for genuinely multiple variants.
  • Align consumers to the canonical defaults — normal weight, --text-body text, --text-icon icons.
  • Verify referenced CSS vars exist — an undefined var silently falls back to currentColor (black-bug).

Testing

Use Vitest. Test files: feature.ts → feature.test.ts. See .cursor/rules/sim-testing.mdc for full details.

Global Mocks (vitest.setup.ts)

@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.)

Standard Test Pattern

/**
 * @vitest-environment node
 */
import { createMockRequest } from '@sim/testing'
import { beforeEach, describe, expect, it, vi } from 'vitest'

const { mockGetSession } = vi.hoisted(() => ({
  mockGetSession: vi.fn(),
}))

vi.mock('@/lib/auth', () => ({
  auth: { api: { getSession: vi.fn() } },
  getSession: mockGetSession,
}))

import { GET } from '@/app/api/my-route/route'

describe('my route', () => {
  beforeEach(() => {
    vi.clearAllMocks()
    mockGetSession.mockResolvedValue({ user: { id: 'user-1' } })
  })
  it('returns data', async () => { ... })
})

Performance Rules

  • NEVER use vi.resetModules() + vi.doMock() + await import() — use vi.hoisted() + vi.mock() + static imports
  • NEVER use vi.importActual() — mock everything explicitly
  • NEVER use mockAuth(), mockConsoleLogger(), setupCommonApiMocks() from @sim/testing — they use vi.doMock() internally
  • Mock heavy deps (@/blocks, @/tools/registry, @/triggers) in tests that don't need them
  • Use @vitest-environment node unless DOM APIs are needed (window, document, FormData)
  • Avoid real timers — use 1ms delays or vi.useFakeTimers()

Use @sim/testing mocks/factories over local test data.

Caching

Never hand-roll TTL arithmetic — a Map plus Date.now() - fetchedAt < TTL re-implements expiry, the ceiling, and coalescing badly. Use lru-cache (a direct dependency), and always set max: ttl alone does not bound memory, so a tenant-keyed cache without a ceiling grows for the life of the process. Prefer fetchMethod + cache.fetch(key) for async read-through — it gives TTL, coalescing, and eviction-on-rejection in one primitive — and compose coalesceLocally around an LRUCache only when a hung producer would otherwise wedge callers for the whole TTL.

First check the thing is a cache at all: a lifecycle map whose entry is deleted when the tracked thing ends (activeStreams, pendingChildRuns) is a plain Map, and giving it a TTL or a ceiling introduces an expiry that races the lifecycle. Cache the gate, never the credential — entitlements tolerate bounded staleness in the safe direction, key material must stay fresh so revocation is immediate. Full decision tree, sizing, !== undefined reads, and the invalidation rule are in .claude/rules/sim-caching.md.

Utils Rules

  • Never create utils.ts for single consumer - inline it
  • Create utils.ts when 2+ files need the same helper
  • Check existing sources in lib/ before duplicating

Adding Integrations

New integrations are built in order: Tools → Block → Icon → (optional) Trigger. Always look up the service's API docs first.

Two hard rules that the skills assume:

  • 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, …).
  • 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.

For 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.

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<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.

Never 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.

Download now (ZIP)

Does it work?

Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.

No reviews yet

People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.

  • It worked
  • Partly
  • It didn't
What did you use it for? e.g. Reviewing pull requests in a Rails app

Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.

Your trail

Files you open will collect here, so you can find them again.

Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.

Now do this for your own.

Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.