Skip to content

Commit f0a4095

Browse files
committed
chore(skills): update checklist for boundary e2e checklist
1 parent b8959eb commit f0a4095

3 files changed

Lines changed: 105 additions & 18 deletions

File tree

AGENTS.md

Lines changed: 42 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -14,12 +14,14 @@ You are a professional software engineer. All code must follow best practices: a
1414
## Architecture
1515

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

2223
### Root Structure
24+
2325
```
2426
apps/
2527
├── sim/ # Next.js app (UI + API routes + workflow editor)
@@ -52,12 +54,14 @@ packages/
5254
```
5355

5456
### Package boundaries
57+
5558
- `apps/* → packages/*` only. Packages never import from `apps/*`.
5659
- Each package has explicit subpath `exports` maps; no barrels that accidentally pull in heavy halves.
5760
- `apps/realtime` intentionally avoids Next.js, React, the block/tool registry, provider SDKs, and the executor. CI enforces this via `scripts/check-monorepo-boundaries.ts` and `scripts/check-realtime-prune-graph.ts`.
5861
- Auth is shared across services via the Better Auth "Shared Database Session" pattern: both apps read the same `BETTER_AUTH_SECRET` and point at the same DB via `@sim/db`.
5962

6063
### Naming Conventions
64+
6165
- Components: PascalCase (`WorkflowList`)
6266
- Hooks: `use` prefix (`useWorkflowOperations`)
6367
- Files: kebab-case (`workflow-list.tsx`)
@@ -80,6 +84,7 @@ import { useWorkflowStore } from '../../../stores/workflows/store'
8084
Use barrel exports (`index.ts`) when a folder has 3+ exports. Do not re-export from non-barrel files; import directly from the source.
8185

8286
### Import Order
87+
8388
1. React/core libraries
8489
2. External libraries
8590
3. UI components (`@/components/emcn`, `@/components/ui`)
@@ -185,6 +190,28 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
185190

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

193+
### Adding a new boundary feature end-to-end
194+
195+
When adding a new route + client surface, follow this order. Each step has one place it lives.
196+
197+
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).
198+
2. **Implement the route** in `apps/sim/app/api/<path>/route.ts`. Auth always runs **before** `parseRequest` — never validate untrusted input before authenticating the caller. The route returns exactly the shape declared in `contract.response.schema`.
199+
3. **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.
200+
4. **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`).
201+
202+
### Schema review checklist (read the contract diff like a DB migration)
203+
204+
LLMs will write contracts that compile but are sloppy. The human reviewer should optimize attention on:
205+
206+
- `**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.
207+
- **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.
208+
- **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.
209+
- **Bounds are set** on arrays (`.min(1)`, `.max(N)`), strings (`.min(1).max(N)` for IDs/names), and numbers (`.min().max()` for limits/sizes).
210+
- `**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.
211+
- **Discriminated unions over plain unions** when the wire has a discriminant field — gives clients exhaustive narrowing.
212+
213+
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.
214+
188215
## Hooks
189216

190217
```typescript
@@ -404,6 +431,7 @@ tools/{service}/
404431
```
405432

406433
**Tool structure:**
434+
407435
```typescript
408436
export const serviceTool: ToolConfig<Params, Response> = {
409437
id: 'service_action',
@@ -442,6 +470,7 @@ Register in `blocks/registry.ts` (alphabetically).
442470
**Important:** `tools.config.tool` runs during serialization (before variable resolution). Never do `Number()` or other type coercions there — dynamic references like `<Block.output>` will be destroyed. Use `tools.config.params` for type coercions (it runs during execution, after variables are resolved).
443471

444472
**SubBlock Properties:**
473+
445474
```typescript
446475
{
447476
id: 'field', title: 'Label', type: 'short-input', placeholder: '...',
@@ -453,6 +482,7 @@ Register in `blocks/registry.ts` (alphabetically).
453482
```
454483

455484
**condition examples:**
485+
456486
- `{ field: 'op', value: 'send' }` - show when op === 'send'
457487
- `{ field: 'op', value: ['a','b'] }` - show when op is 'a' OR 'b'
458488
- `{ field: 'op', value: 'x', not: true }` - show when op !== 'x'
@@ -461,6 +491,7 @@ Register in `blocks/registry.ts` (alphabetically).
461491
**dependsOn:** `['field']` or `{ all: ['a'], any: ['b', 'c'] }`
462492

463493
**File Input Pattern (basic/advanced mode):**
494+
464495
```typescript
465496
// Basic: file-upload UI
466497
{ id: 'uploadFile', type: 'file-upload', canonicalParamId: 'file', mode: 'basic' },
@@ -469,6 +500,7 @@ Register in `blocks/registry.ts` (alphabetically).
469500
```
470501

471502
In `tools.config.tool`, normalize with:
503+
472504
```typescript
473505
import { normalizeFileInput } from '@/blocks/utils'
474506
const file = normalizeFileInput(params.uploadFile || params.fileRef, { single: true })
@@ -498,12 +530,13 @@ Register in `triggers/registry.ts`.
498530

499531
### Integration Checklist
500532

501-
- [ ] Look up API docs
502-
- [ ] Create `tools/{service}/` with types and tools
503-
- [ ] Register tools in `tools/registry.ts`
504-
- [ ] Add icon to `components/icons.tsx`
505-
- [ ] Create block in `blocks/blocks/{service}.ts`
506-
- [ ] Register block in `blocks/registry.ts`
507-
- [ ] (Optional) Create and register triggers
508-
- [ ] (If file uploads) Create internal API route with `downloadFileFromStorage`
509-
- [ ] (If file uploads) Use `normalizeFileInput` in block config
533+
- Look up API docs
534+
- Create `tools/{service}/` with types and tools
535+
- Register tools in `tools/registry.ts`
536+
- Add icon to `components/icons.tsx`
537+
- Create block in `blocks/blocks/{service}.ts`
538+
- Register block in `blocks/registry.ts`
539+
- (Optional) Create and register triggers
540+
- (If file uploads) Create internal API route with `downloadFileFromStorage`
541+
- (If file uploads) Use `normalizeFileInput` in block config
542+

CLAUDE.md

Lines changed: 41 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,14 @@ You are a professional software engineer. All code must follow best practices: a
1616
## Architecture
1717

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

2425
### Root Structure
26+
2527
```
2628
apps/sim/
2729
├── app/ # Next.js app router (pages, API routes)
@@ -37,6 +39,7 @@ apps/sim/
3739
```
3840

3941
### Naming Conventions
42+
4043
- Components: PascalCase (`WorkflowList`)
4144
- Hooks: `use` prefix (`useWorkflowOperations`)
4245
- Files: kebab-case (`workflow-list.tsx`)
@@ -59,6 +62,7 @@ import { useWorkflowStore } from '../../../stores/workflows/store'
5962
Use barrel exports (`index.ts`) when a folder has 3+ exports. Do not re-export from non-barrel files; import directly from the source.
6063

6164
### Import Order
65+
6266
1. React/core libraries
6367
2. External libraries
6468
3. UI components (`@/components/emcn`, `@/components/ui`)
@@ -176,6 +180,28 @@ Routes under `apps/sim/app/api/v1/**` use the shared middleware in `apps/sim/app
176180

177181
Never export a bare `async function GET/POST/...` — always use `export const METHOD = withRouteHandler(...)`.
178182

183+
### Adding a new boundary feature end-to-end
184+
185+
When adding a new route + client surface, follow this order. Each step has one place it lives.
186+
187+
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).
188+
2. **Implement the route** in `apps/sim/app/api/<path>/route.ts`. Auth always runs **before** `parseRequest` — never validate untrusted input before authenticating the caller. The route returns exactly the shape declared in `contract.response.schema`.
189+
3. **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.
190+
4. **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`).
191+
192+
### Schema review checklist (read the contract diff like a DB migration)
193+
194+
LLMs will write contracts that compile but are sloppy. The human reviewer should optimize attention on:
195+
196+
- `**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.
197+
- **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.
198+
- **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.
199+
- **Bounds are set** on arrays (`.min(1)`, `.max(N)`), strings (`.min(1).max(N)` for IDs/names), and numbers (`.min().max()` for limits/sizes).
200+
- `**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.
201+
- **Discriminated unions over plain unions** when the wire has a discriminant field — gives clients exhaustive narrowing.
202+
203+
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.
204+
179205
## Hooks
180206

181207
```typescript
@@ -395,6 +421,7 @@ tools/{service}/
395421
```
396422

397423
**Tool structure:**
424+
398425
```typescript
399426
export const serviceTool: ToolConfig<Params, Response> = {
400427
id: 'service_action',
@@ -433,6 +460,7 @@ Register in `blocks/registry.ts` (alphabetically).
433460
**Important:** `tools.config.tool` runs during serialization (before variable resolution). Never do `Number()` or other type coercions there — dynamic references like `<Block.output>` will be destroyed. Use `tools.config.params` for type coercions (it runs during execution, after variables are resolved).
434461

435462
**SubBlock Properties:**
463+
436464
```typescript
437465
{
438466
id: 'field', title: 'Label', type: 'short-input', placeholder: '...',
@@ -444,6 +472,7 @@ Register in `blocks/registry.ts` (alphabetically).
444472
```
445473

446474
**condition examples:**
475+
447476
- `{ field: 'op', value: 'send' }` - show when op === 'send'
448477
- `{ field: 'op', value: ['a','b'] }` - show when op is 'a' OR 'b'
449478
- `{ field: 'op', value: 'x', not: true }` - show when op !== 'x'
@@ -452,6 +481,7 @@ Register in `blocks/registry.ts` (alphabetically).
452481
**dependsOn:** `['field']` or `{ all: ['a'], any: ['b', 'c'] }`
453482

454483
**File Input Pattern (basic/advanced mode):**
484+
455485
```typescript
456486
// Basic: file-upload UI
457487
{ id: 'uploadFile', type: 'file-upload', canonicalParamId: 'file', mode: 'basic' },
@@ -460,6 +490,7 @@ Register in `blocks/registry.ts` (alphabetically).
460490
```
461491

462492
In `tools.config.tool`, normalize with:
493+
463494
```typescript
464495
import { normalizeFileInput } from '@/blocks/utils'
465496
const file = normalizeFileInput(params.uploadFile || params.fileRef, { single: true })
@@ -489,12 +520,13 @@ Register in `triggers/registry.ts`.
489520

490521
### Integration Checklist
491522

492-
- [ ] Look up API docs
493-
- [ ] Create `tools/{service}/` with types and tools
494-
- [ ] Register tools in `tools/registry.ts`
495-
- [ ] Add icon to `components/icons.tsx`
496-
- [ ] Create block in `blocks/blocks/{service}.ts`
497-
- [ ] Register block in `blocks/registry.ts`
498-
- [ ] (Optional) Create and register triggers
499-
- [ ] (If file uploads) Create internal API route with `downloadFileFromStorage`
500-
- [ ] (If file uploads) Use `normalizeFileInput` in block config
523+
- Look up API docs
524+
- Create `tools/{service}/` with types and tools
525+
- Register tools in `tools/registry.ts`
526+
- Add icon to `components/icons.tsx`
527+
- Create block in `blocks/blocks/{service}.ts`
528+
- Register block in `blocks/registry.ts`
529+
- (Optional) Create and register triggers
530+
- (If file uploads) Create internal API route with `downloadFileFromStorage`
531+
- (If file uploads) Use `normalizeFileInput` in block config
532+

apps/sim/AGENTS.md

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,28 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
136136

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

139+
### Adding a new boundary feature end-to-end
140+
141+
When adding a new route + client surface, follow this order. Each step has one place it lives.
142+
143+
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).
144+
2. **Implement the route** in `apps/sim/app/api/<path>/route.ts`. Auth always runs **before** `parseRequest` — never validate untrusted input before authenticating the caller. The route returns exactly the shape declared in `contract.response.schema`.
145+
3. **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.
146+
4. **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`).
147+
148+
### Schema review checklist (read the contract diff like a DB migration)
149+
150+
LLMs will write contracts that compile but are sloppy. The human reviewer should optimize attention on:
151+
152+
- **`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.
153+
- **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.
154+
- **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.
155+
- **Bounds are set** on arrays (`.min(1)`, `.max(N)`), strings (`.min(1).max(N)` for IDs/names), and numbers (`.min().max()` for limits/sizes).
156+
- **`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.
157+
- **Discriminated unions over plain unions** when the wire has a discriminant field — gives clients exhaustive narrowing.
158+
159+
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.
160+
139161
## React Query Client Boundary
140162

141163
Hooks in `apps/sim/hooks/queries/**` 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`:

0 commit comments

Comments
 (0)