@stonepandastudio/cairn 0.4.2 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +60 -23
- package/bin/cairn.js +13 -5
- package/lib/doctor/index.js +6 -1
- package/lib/init.js +15 -1
- package/lib/manifest.js +33 -0
- package/lib/render/cli.js +116 -0
- package/lib/render/engine.js +148 -0
- package/lib/render/index.js +220 -0
- package/lib/sync/cli.js +197 -0
- package/lib/sync/index.js +249 -0
- package/package.json +2 -1
- package/presets/EXTRACTION.md +210 -0
- package/presets/README.md +86 -0
- package/presets/angular/code-guidelines.md +197 -0
- package/presets/angular/slots/architect-discussion-topics.md +10 -0
- package/presets/angular/slots/architect-mandatory-docs.md +5 -0
- package/presets/angular/slots/architect-references.md +4 -0
- package/presets/angular/slots/implementation-reference.md +34 -0
- package/presets/angular/slots/key-patterns.md +11 -0
- package/presets/angular/slots/plan-step-ordering.md +11 -0
- package/presets/angular/slots/review-checklist.md +16 -0
- package/presets/angular/variants/i18n-external-service.md +11 -0
- package/presets/angular/variants/i18n-glossr.md +63 -0
- package/presets/core/AGENTS.md +49 -0
- package/presets/core/README.md +35 -0
- package/presets/core/WORKFLOW.md +56 -0
- package/presets/core/agents/architect.md +269 -0
- package/presets/core/agents/developer.md +145 -0
- package/presets/core/agents/reviewer.md +167 -0
- package/presets/core/commands/_stub.md +7 -0
- package/presets/core/workflow.json +45 -0
- package/presets/drizzle/code-guidelines.md +33 -0
- package/presets/drizzle/slots/architect-discussion-topics.md +4 -0
- package/presets/drizzle/slots/architect-mandatory-docs.md +4 -0
- package/presets/drizzle/slots/implementation-reference.md +17 -0
- package/presets/drizzle/slots/key-patterns.md +7 -0
- package/presets/drizzle/slots/review-checklist.md +10 -0
- package/presets/nestjs/code-guidelines.md +273 -0
- package/presets/nestjs/slots/architect-discussion-topics.md +4 -0
- package/presets/nestjs/slots/architect-mandatory-docs.md +5 -0
- package/presets/nestjs/slots/architect-references.md +5 -0
- package/presets/nestjs/slots/implementation-reference.md +45 -0
- package/presets/nestjs/slots/key-patterns.md +11 -0
- package/presets/nestjs/slots/plan-step-ordering.md +12 -0
- package/presets/nestjs/slots/review-checklist.md +12 -0
- package/presets/nestjs/variants/validation-class-validator.md +120 -0
- package/presets/nestjs/variants/validation-zod.md +194 -0
- package/presets/nextjs/code-guidelines.md +45 -0
- package/presets/nextjs/slots/architect-discussion-topics.md +5 -0
- package/presets/nextjs/slots/architect-mandatory-docs.md +3 -0
- package/presets/nextjs/slots/architect-references.md +6 -0
- package/presets/nextjs/slots/implementation-reference.md +24 -0
- package/presets/nextjs/slots/key-patterns.md +8 -0
- package/presets/nextjs/slots/plan-step-ordering.md +11 -0
- package/presets/nextjs/slots/review-checklist.md +11 -0
- package/presets/react/code-guidelines.md +46 -0
- package/presets/react/slots/architect-discussion-topics.md +5 -0
- package/presets/react/slots/architect-references.md +5 -0
- package/presets/react/slots/implementation-reference.md +26 -0
- package/presets/react/slots/key-patterns.md +8 -0
- package/presets/react/slots/plan-step-ordering.md +9 -0
- package/presets/react/slots/review-checklist.md +10 -0
- package/presets/tailwind/code-guidelines.md +28 -0
- package/presets/tailwind/slots/implementation-reference.md +8 -0
- package/presets/tailwind/slots/key-patterns.md +5 -0
- package/presets/tailwind/slots/review-checklist.md +8 -0
- package/presets/typeorm/code-guidelines.md +329 -0
- package/presets/typeorm/slots/architect-discussion-topics.md +4 -0
- package/presets/typeorm/slots/architect-mandatory-docs.md +3 -0
- package/presets/typeorm/slots/implementation-reference.md +19 -0
- package/presets/typeorm/slots/key-patterns.md +8 -0
- package/presets/typeorm/slots/review-checklist.md +8 -0
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
<!-- cairn preset: nestjs — fills {{> stack/review-checklist }} in the core reviewer.
|
|
2
|
+
Framework rules only. A repo on `--stack nestjs,typeorm` also gets the ORM
|
|
3
|
+
rows from presets/typeorm/slots/review-checklist.md (the renderer concatenates
|
|
4
|
+
this slot from every stack entry). Response-mapping and validation-decorator
|
|
5
|
+
rows come from the chosen validation-stack variant, not here. -->
|
|
6
|
+
|
|
7
|
+
| No `any` type — use proper TypeScript types | All changed files |
|
|
8
|
+
| Enums in `enums/` folder, constants in `constants/` folder, kebab-case filenames | Feature module |
|
|
9
|
+
| No magic strings — use enum values or named constants | Anywhere |
|
|
10
|
+
| API modules contain no business logic (thin controllers, delegate to feature services) | Controller / API service files |
|
|
11
|
+
| Feature modules export only the services that API modules need | Module files |
|
|
12
|
+
| NestJS built-in exceptions used for errors (`NotFoundException`, `BadRequestException`, …) | Service and controller files |
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
<!-- cairn variant: validation-stack = class-validator. Fills the dto-naming,
|
|
2
|
+
dto-definition, validation-stack, controller-response,
|
|
3
|
+
response-transformation and pagination-dtos slots in
|
|
4
|
+
presets/node/code-guidelines.md.
|
|
5
|
+
In use by: snap-backend. Requires `class-validator` + `class-transformer`
|
|
6
|
+
and the project's own `@ResponseMapper` decorator. -->
|
|
7
|
+
|
|
8
|
+
## slot: dto-naming
|
|
9
|
+
|
|
10
|
+
* Use operation-prefixed names for incoming request data:
|
|
11
|
+
- `CreateProjectDto`, `UpdateUserDto`
|
|
12
|
+
- File: `create-feature.dto.ts`, `update-feature.dto.ts`
|
|
13
|
+
* Use the `Response` suffix for server response shapes:
|
|
14
|
+
- `UserResponse`, `ProjectListResponse`
|
|
15
|
+
- File: `feature-response.dto.ts`
|
|
16
|
+
|
|
17
|
+
## slot: dto-definition
|
|
18
|
+
|
|
19
|
+
* Use class-validator decorators: `@IsString()`, `@IsNumber()`, `@IsEmail()`, `@IsOptional()`, `@IsNotEmpty()`
|
|
20
|
+
* Use class-transformer decorators: `@Type()`, `@Exclude()`, `@Expose()`
|
|
21
|
+
* Add Swagger documentation: `@ApiProperty()`, `@ApiPropertyOptional()`
|
|
22
|
+
* Provide validation error messages where appropriate
|
|
23
|
+
|
|
24
|
+
## slot: validation-stack
|
|
25
|
+
|
|
26
|
+
* Use class-validator decorators on all DTO properties
|
|
27
|
+
* Use `ValidationPipe` globally or per-controller
|
|
28
|
+
* Create custom validators for complex validation logic
|
|
29
|
+
|
|
30
|
+
## slot: controller-response
|
|
31
|
+
|
|
32
|
+
* Apply `@UsePipes(ValidationPipe)` for automatic DTO validation
|
|
33
|
+
* Annotate the response shape with `@ResponseMapper(ResponseDtoClass)` on every endpoint that returns data
|
|
34
|
+
|
|
35
|
+
## slot: response-transformation
|
|
36
|
+
|
|
37
|
+
**IMPORTANT**: The `@ResponseMapper(ResponseDtoClass)` decorator automatically handles entity-to-DTO transformation using class-transformer.
|
|
38
|
+
|
|
39
|
+
**How it works**:
|
|
40
|
+
* The decorator uses `@Expose()` decorators in the response DTO to pick which fields to include
|
|
41
|
+
* The decorator uses `@Type()` decorators to handle nested object transformations
|
|
42
|
+
* You **DO NOT** need to manually create mapper functions to pick fields
|
|
43
|
+
|
|
44
|
+
**Best Practices**:
|
|
45
|
+
* Return entities or plain objects from service methods
|
|
46
|
+
* Only perform minimal transformations needed for data structure (e.g., flattening nested properties)
|
|
47
|
+
* Let `@ResponseMapper` handle field selection and type transformation
|
|
48
|
+
* Do **NOT** create separate mapper functions just to pick fields - the decorator does this automatically
|
|
49
|
+
|
|
50
|
+
**Example - Correct approach**:
|
|
51
|
+
```typescript
|
|
52
|
+
// Service method - minimal transformation
|
|
53
|
+
async getDevicesByProjects() {
|
|
54
|
+
const devices = await this.siteDeviceService.getDevicesByProjects();
|
|
55
|
+
|
|
56
|
+
// Only flatten nested structures that don't match DTO
|
|
57
|
+
return devices.map(device => ({
|
|
58
|
+
...device,
|
|
59
|
+
site: device.site ? {
|
|
60
|
+
...device.site,
|
|
61
|
+
city: device.site.city?.name || null // Flatten CityEntity to string
|
|
62
|
+
} : null,
|
|
63
|
+
}));
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Controller - @ResponseMapper handles field picking
|
|
67
|
+
@Get('by-projects')
|
|
68
|
+
@ResponseMapper(DeviceWithProjectsResponse)
|
|
69
|
+
async getDevicesByProjects(@Query() query: QueryDto) {
|
|
70
|
+
return this.service.getDevicesByProjects(query);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Response DTO - @Expose() picks fields automatically
|
|
74
|
+
export class DeviceWithProjectsResponse {
|
|
75
|
+
@Expose() id: string;
|
|
76
|
+
@Expose() description: string;
|
|
77
|
+
// Only exposed fields will be returned
|
|
78
|
+
}
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
**Example - Incorrect approach** (avoid this):
|
|
82
|
+
```typescript
|
|
83
|
+
// ❌ DON'T create mapper functions just to pick fields
|
|
84
|
+
function mapToResponse(device) {
|
|
85
|
+
return {
|
|
86
|
+
id: device.id, // @ResponseMapper does this
|
|
87
|
+
description: device.description, // automatically
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
## slot: pagination-dtos
|
|
93
|
+
|
|
94
|
+
**Query params** — declare `page` and `pageSize` with `@Type(() => Number)` so query strings coerce:
|
|
95
|
+
|
|
96
|
+
```typescript
|
|
97
|
+
export class FooListQuery {
|
|
98
|
+
@Type(() => Number)
|
|
99
|
+
@IsInt()
|
|
100
|
+
@Min(1)
|
|
101
|
+
page = 1;
|
|
102
|
+
|
|
103
|
+
@Type(() => Number)
|
|
104
|
+
@IsInt()
|
|
105
|
+
@Min(1)
|
|
106
|
+
@Max(100)
|
|
107
|
+
pageSize = 50;
|
|
108
|
+
}
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
**Response DTO** — expose the envelope fields and type the items:
|
|
112
|
+
|
|
113
|
+
```typescript
|
|
114
|
+
export class FooListResponse {
|
|
115
|
+
@Expose() @Type(() => FooResponse) items: FooResponse[];
|
|
116
|
+
@Expose() total: number;
|
|
117
|
+
@Expose() skip: number;
|
|
118
|
+
@Expose() take: number;
|
|
119
|
+
}
|
|
120
|
+
```
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
<!-- cairn variant: validation-stack = zod. Fills the dto-naming, dto-definition,
|
|
2
|
+
validation-stack, controller-response, response-transformation and
|
|
3
|
+
pagination-dtos slots in presets/node/code-guidelines.md.
|
|
4
|
+
In use by: glossr-api. Requires `zod` + `nestjs-zod`. -->
|
|
5
|
+
|
|
6
|
+
## slot: dto-naming
|
|
7
|
+
|
|
8
|
+
* Use **`Payload` prefix** for incoming request data (body, query params):
|
|
9
|
+
- `RegisterOwnerPayload`, `CreateProjectPayload`, `UpdateUserPayload`
|
|
10
|
+
- File: `register-owner-payload.dto.ts`
|
|
11
|
+
* Use **`Response` prefix** for server response shapes:
|
|
12
|
+
- `RegisterOwnerResponse`, `UserResponse`, `ProjectListResponse`
|
|
13
|
+
- File: `register-owner-response.dto.ts`
|
|
14
|
+
|
|
15
|
+
**Examples**:
|
|
16
|
+
```typescript
|
|
17
|
+
// Request DTO
|
|
18
|
+
export class RegisterOwnerPayload extends createZodDto(RegisterOwnerSchema) {}
|
|
19
|
+
|
|
20
|
+
// Response DTO
|
|
21
|
+
export class RegisterOwnerResponse extends createZodDto(RegisterOwnerResponseSchema) {}
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## slot: dto-definition
|
|
25
|
+
|
|
26
|
+
* Use **Zod schemas** with `nestjs-zod` integration: `z.string()`, `z.number()`, `z.email()`, `z.optional()`, etc.
|
|
27
|
+
* Create DTOs by extending `createZodDto()` from Zod schemas
|
|
28
|
+
* Add Swagger documentation via Zod schema metadata or `@ApiProperty()` decorators
|
|
29
|
+
* Provide validation error messages in Zod schema definitions
|
|
30
|
+
|
|
31
|
+
**Example**:
|
|
32
|
+
```typescript
|
|
33
|
+
import { z } from 'zod';
|
|
34
|
+
import { createZodDto } from 'nestjs-zod';
|
|
35
|
+
|
|
36
|
+
const CreateUserSchema = z.object({
|
|
37
|
+
email: z.string().email('Invalid email format'),
|
|
38
|
+
password: z.string().min(8, 'Password must be at least 8 characters'),
|
|
39
|
+
name: z.string().min(1, 'Name is required'),
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
export class CreateUserDto extends createZodDto(CreateUserSchema) {}
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## slot: validation-stack
|
|
46
|
+
|
|
47
|
+
* Use **Zod schemas** for all DTO validation
|
|
48
|
+
* Use `ZodValidationPipe` (from `nestjs-zod`) globally or per-controller
|
|
49
|
+
* Create custom Zod schemas for complex validation logic
|
|
50
|
+
* Provide clear, user-friendly validation error messages in schema definitions
|
|
51
|
+
|
|
52
|
+
## slot: controller-response
|
|
53
|
+
|
|
54
|
+
* **ALWAYS** use `@ZodResponse({ type: MyResponse })` on every endpoint that returns data — do NOT add explicit TypeScript return types when using `@ZodResponse` as it infers them
|
|
55
|
+
|
|
56
|
+
**`@ZodResponse` rules:**
|
|
57
|
+
* Replaces `@ZodSerializerDto`, `@ApiResponse`, and the return type annotation in one decorator
|
|
58
|
+
* Keeps run-time serialization, compile-time types, and OpenAPI docs in sync
|
|
59
|
+
* For arrays use `[MyDto]` syntax: `@ZodResponse({ type: [UserResponse] })`
|
|
60
|
+
* For non-200 status codes: `@ZodResponse({ status: 201, type: MyResponse })`
|
|
61
|
+
* For endpoints returning `void` (e.g. DELETE), omit `@ZodResponse`
|
|
62
|
+
* For endpoints returning raw streams/files (using `@Res()`), omit `@ZodResponse`
|
|
63
|
+
|
|
64
|
+
**Example**:
|
|
65
|
+
```typescript
|
|
66
|
+
import { ZodResponse } from 'nestjs-zod';
|
|
67
|
+
|
|
68
|
+
@Controller('users')
|
|
69
|
+
export class UserController {
|
|
70
|
+
@Get('me')
|
|
71
|
+
@UseGuards(JwtAuthGuard)
|
|
72
|
+
@ZodResponse({ type: UserResponse })
|
|
73
|
+
async getMe(@CurrentUser('sub') userId: string) {
|
|
74
|
+
return this.userService.findById(userId);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
@Get()
|
|
78
|
+
@ZodResponse({ type: [UserResponse] })
|
|
79
|
+
async listUsers() {
|
|
80
|
+
return this.userService.findAll();
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
@Post()
|
|
84
|
+
@ZodResponse({ status: 201, type: UserResponse })
|
|
85
|
+
async createUser(@Body() dto: CreateUserPayload) {
|
|
86
|
+
return this.userService.create(dto);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
@Delete(':id')
|
|
90
|
+
// No @ZodResponse — returns void
|
|
91
|
+
async deleteUser(@Param('id') id: string): Promise<void> {
|
|
92
|
+
await this.userService.delete(id);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
## slot: response-transformation
|
|
98
|
+
|
|
99
|
+
**IMPORTANT**: Use Zod schemas for response DTOs to automatically handle entity-to-DTO transformation with type safety.
|
|
100
|
+
|
|
101
|
+
**How it works**:
|
|
102
|
+
* Response DTOs extend `createZodDto()` from Zod schemas
|
|
103
|
+
* Zod schemas pick which fields to include in the response
|
|
104
|
+
* Automatic type inference from Zod schemas
|
|
105
|
+
* Built-in serialization support via `nestjs-zod`
|
|
106
|
+
|
|
107
|
+
**Best Practices**:
|
|
108
|
+
* Return entities or plain objects from service methods
|
|
109
|
+
* Define response DTOs as separate Zod schemas
|
|
110
|
+
* Only perform minimal transformations needed for data structure (e.g., flattening nested properties)
|
|
111
|
+
* Let Zod handle field selection and type transformation
|
|
112
|
+
* Do **NOT** create separate mapper functions just to pick fields
|
|
113
|
+
|
|
114
|
+
**Example - Correct approach**:
|
|
115
|
+
```typescript
|
|
116
|
+
import { z } from 'zod';
|
|
117
|
+
import { createZodDto } from 'nestjs-zod';
|
|
118
|
+
|
|
119
|
+
// Response schema - pick fields that should be returned
|
|
120
|
+
const DeviceWithProjectsSchema = z.object({
|
|
121
|
+
id: z.string(),
|
|
122
|
+
description: z.string(),
|
|
123
|
+
site: z.object({
|
|
124
|
+
id: z.string(),
|
|
125
|
+
name: z.string(),
|
|
126
|
+
city: z.string().nullable(),
|
|
127
|
+
}).nullable(),
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
export class DeviceWithProjectsResponse extends createZodDto(DeviceWithProjectsSchema) {}
|
|
131
|
+
|
|
132
|
+
// Service method - minimal transformation
|
|
133
|
+
async getDevicesByProjects() {
|
|
134
|
+
const devices = await this.siteDeviceService.getDevicesByProjects();
|
|
135
|
+
|
|
136
|
+
// Only flatten nested structures that don't match schema
|
|
137
|
+
return devices.map(device => ({
|
|
138
|
+
...device,
|
|
139
|
+
site: device.site ? {
|
|
140
|
+
...device.site,
|
|
141
|
+
city: device.site.city?.name || null // Flatten CityEntity to string
|
|
142
|
+
} : null,
|
|
143
|
+
}));
|
|
144
|
+
}
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
**Example - Incorrect approach** (avoid this):
|
|
148
|
+
```typescript
|
|
149
|
+
// ❌ DON'T create mapper functions just to pick fields
|
|
150
|
+
function mapToResponse(device) {
|
|
151
|
+
return {
|
|
152
|
+
id: device.id, // The Zod schema does this automatically
|
|
153
|
+
description: device.description,
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
## slot: pagination-dtos
|
|
159
|
+
|
|
160
|
+
**Query params** — always `page` + `pageSize` with `z.coerce.number()` (coercion required because query params arrive as strings):
|
|
161
|
+
|
|
162
|
+
```typescript
|
|
163
|
+
// src/api/modules/fooApi/dtos/foo-list-query.dto.ts
|
|
164
|
+
const FooListQuerySchema = z.object({
|
|
165
|
+
page: z.coerce.number().int().min(1).default(1),
|
|
166
|
+
pageSize: z.coerce.number().int().min(1).max(100).default(50),
|
|
167
|
+
// add sorting / filter fields as needed
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
export class FooListQuery extends createZodDto(FooListQuerySchema) {}
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
**Response DTO** — wrap the item schema with `paginatedResponseSchema`:
|
|
174
|
+
|
|
175
|
+
```typescript
|
|
176
|
+
// src/api/modules/fooApi/dtos/foo-list-response.dto.ts
|
|
177
|
+
import { createZodDto } from 'nestjs-zod';
|
|
178
|
+
import { paginatedResponseSchema } from '@common/dtos/paginated-response.schema';
|
|
179
|
+
import { FooResponseSchema } from './foo-response.dto';
|
|
180
|
+
|
|
181
|
+
const FooListResponseSchema = paginatedResponseSchema(FooResponseSchema);
|
|
182
|
+
|
|
183
|
+
export class FooListResponse extends createZodDto(FooListResponseSchema) {}
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
**Controller**:
|
|
187
|
+
|
|
188
|
+
```typescript
|
|
189
|
+
@Get()
|
|
190
|
+
@ZodResponse({ type: FooListResponse })
|
|
191
|
+
async list(@Query() query: FooListQuery) {
|
|
192
|
+
return this.fooService.list(query);
|
|
193
|
+
}
|
|
194
|
+
```
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# Next.js Guidelines
|
|
2
|
+
|
|
3
|
+
<!-- cairn preset: nextjs. The App-Router framework layer, added on top of the
|
|
4
|
+
`react` component base: `--stack nextjs,react,tailwind,drizzle`. It owns the
|
|
5
|
+
server/client boundary, Server Actions, Route Handlers, and the App-Router
|
|
6
|
+
file conventions — not component/hook/state basics, which stay in
|
|
7
|
+
`presets/react`. Seeded 2026-09-09 from charityiq-app (Next.js 16, Sprint 0).
|
|
8
|
+
Single consumer; refine when a second Next.js repo appears. -->
|
|
9
|
+
|
|
10
|
+
## The framework moves fast
|
|
11
|
+
|
|
12
|
+
Next.js's App Router, `"use cache"`, and the RSC model change between minor
|
|
13
|
+
versions and are often ahead of training data. Before writing anything non-obvious,
|
|
14
|
+
check `node_modules/next/dist/**` or the version's docs. Heed deprecation notices.
|
|
15
|
+
|
|
16
|
+
## Server / Client boundary
|
|
17
|
+
|
|
18
|
+
* **Server Components by default.** Add `"use client"` only for interactivity, browser APIs, or hooks (`useState`, `useEffect`, event handlers).
|
|
19
|
+
* A `"use client"` module must not import server-only code — DB clients, secrets, anything marked `server-only`. Push the client boundary as far down the tree as possible; a page can be a Server Component that renders a small client island.
|
|
20
|
+
* Never read a secret in a Client Component. Client-exposed env vars carry the `NEXT_PUBLIC_` prefix; everything else is server-only.
|
|
21
|
+
|
|
22
|
+
## Server Actions
|
|
23
|
+
|
|
24
|
+
* `'use server'` at the top of the module (or the function). Actions run on the server; treat every argument as untrusted input and validate it.
|
|
25
|
+
* An action is a mutation entry point — it does the whole unit of work (validate → authorize → mutate → revalidate) and returns a serializable result.
|
|
26
|
+
* Call `revalidatePath` / `revalidateTag` after a write so the RSC cache reflects it.
|
|
27
|
+
* Do not use an action as a generic RPC for reads — read in the Server Component.
|
|
28
|
+
|
|
29
|
+
## Route Handlers
|
|
30
|
+
|
|
31
|
+
* `app/**/route.ts` for webhooks, third-party callbacks, and non-action HTTP (file downloads, health checks).
|
|
32
|
+
* Verify the caller (signature, secret, or auth) before doing any work.
|
|
33
|
+
* Keep them thin — parse, authorize, delegate to a service, respond.
|
|
34
|
+
|
|
35
|
+
## App Router structure
|
|
36
|
+
|
|
37
|
+
* `page.tsx` / `layout.tsx` / `loading.tsx` / `error.tsx` / `not-found.tsx` per segment; route groups `(name)/` for layout scoping without a URL segment; `[param]` / `[...slug]` for dynamic routes.
|
|
38
|
+
* Colocate a segment's server code (`actions.ts`, `queries.ts`) inside its folder.
|
|
39
|
+
* `Metadata` export (static or `generateMetadata`) on every routable page for SEO.
|
|
40
|
+
* `next/font` for fonts, `next/image` for images — never a raw `<img>` for content images or a `<link>` font.
|
|
41
|
+
|
|
42
|
+
## Data fetching
|
|
43
|
+
|
|
44
|
+
* Fetch in Server Components and Server Actions, not in client `useEffect`.
|
|
45
|
+
* Reach for the framework's caching primitives (`fetch` cache options, `revalidate`, `unstable_cache` / `"use cache"` where stable) rather than a bespoke client cache.
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
<!-- cairn preset: nextjs — appended to the architect-discussion-topics slot on a
|
|
2
|
+
`--stack nextjs,react,...` repo. -->
|
|
3
|
+
- **Server vs client** — does this segment need interactivity, or can it stay a Server Component
|
|
4
|
+
- **Action vs Route Handler** — a user-triggered mutation (action) or an external/webhook entry point (handler)
|
|
5
|
+
- **Cache / revalidation** — what has to be re-fetched after this write, and how it's tagged
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
<!-- cairn preset: nextjs — fills {{> stack/architect-references }} (A.1).
|
|
2
|
+
Primary-stack-only. This is a fullstack repo — one place holds both the UI
|
|
3
|
+
and the server code. -->
|
|
4
|
+
- The App Router tree (`app/`) — pages, layouts, colocated `actions.ts` / `queries.ts`
|
|
5
|
+
- Shared server modules (`lib/`) — DB, auth, integrations, the AI client
|
|
6
|
+
- Context docs in `ai/contexts/**` — entities, features, page/flow business logic
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
<!-- cairn preset: nextjs — appended to the implementation-reference slot on a
|
|
2
|
+
`--stack nextjs,react,...` repo. -->
|
|
3
|
+
|
|
4
|
+
### Server / Client
|
|
5
|
+
- Default Server Component; `"use client"` at the smallest island that needs it
|
|
6
|
+
- Client boundary never pulls in DB clients, secrets, or `server-only` modules
|
|
7
|
+
- Pass data down as props from the Server Component; don't refetch on the client
|
|
8
|
+
|
|
9
|
+
### Server Actions
|
|
10
|
+
- `'use server'`; validate args; do the whole unit of work (authorize → mutate → revalidate)
|
|
11
|
+
- `revalidatePath` / `revalidateTag` after a write; return a serializable result
|
|
12
|
+
- Colocate as `app/<segment>/actions.ts`
|
|
13
|
+
|
|
14
|
+
### Route Handlers
|
|
15
|
+
- `app/**/route.ts` for webhooks and non-action HTTP; verify the caller first; keep thin
|
|
16
|
+
|
|
17
|
+
### App Router
|
|
18
|
+
- `page` / `layout` / `loading` / `error` / `not-found` per segment
|
|
19
|
+
- Route groups `(name)/`, dynamic `[param]` / `[...slug]`
|
|
20
|
+
- `Metadata` / `generateMetadata` on every routable page
|
|
21
|
+
- `next/font`, `next/image` — no raw `<img>` / `<link>` font
|
|
22
|
+
|
|
23
|
+
### Data
|
|
24
|
+
- Fetch in RSC / Actions; use framework cache primitives, not a client cache
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
<!-- cairn preset: nextjs — appended to the key-patterns slot on a
|
|
2
|
+
`--stack nextjs,react,...` repo. -->
|
|
3
|
+
|
|
4
|
+
App Router structure, the server/client boundary, Server Action and Route Handler
|
|
5
|
+
conventions: **`ai/infrastructure/code-guidelines.md`** § Next.js.
|
|
6
|
+
|
|
7
|
+
Existing routes and their layouts: **`app/`** — mirror the nearest segment's shape
|
|
8
|
+
(colocated `actions.ts` / `queries.ts`, `loading` / `error` files) before adding one.
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
<!-- cairn preset: nextjs — fills {{> stack/plan-step-ordering }} in the core
|
|
2
|
+
architect's Plan format. Primary-stack-only. Put `nextjs` first in `--stack`
|
|
3
|
+
so this fullstack ordering wins over the plain-react one. -->
|
|
4
|
+
1. Types / schema / DB migration
|
|
5
|
+
2. Server modules — queries, integrations, `lib/` helpers
|
|
6
|
+
3. Server Actions / Route Handlers
|
|
7
|
+
4. Server Components — pages, layouts, data loading
|
|
8
|
+
5. Client islands — interactive components
|
|
9
|
+
6. Styling and responsive / dark-mode passes
|
|
10
|
+
7. Tests — Vitest units, then a Playwright flow
|
|
11
|
+
8. Context doc updates
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
<!-- cairn preset: nextjs — appended to the review-checklist slot on a
|
|
2
|
+
`--stack nextjs,react,...` repo. Framework-boundary rules only. -->
|
|
3
|
+
|
|
4
|
+
| Server Component by default — `"use client"` only for interactivity / browser APIs / hooks | component `.tsx` |
|
|
5
|
+
| `"use client"` module imports no server-only code (DB client, secrets, `server-only`) | client components |
|
|
6
|
+
| No secret read in a Client Component; client env vars are `NEXT_PUBLIC_`-prefixed | `.tsx`, env usage |
|
|
7
|
+
| Server Action starts with `'use server'` and validates every argument as untrusted | `actions.ts` |
|
|
8
|
+
| Server Action calls `revalidatePath` / `revalidateTag` after a write | `actions.ts` |
|
|
9
|
+
| Route Handler verifies the caller before any work | `app/**/route.ts` |
|
|
10
|
+
| Data fetched in a Server Component / Action, not a client `useEffect` | component / hook files |
|
|
11
|
+
| Routable page exports `Metadata`; content images use `next/image`, fonts use `next/font` | `page.tsx`, `layout.tsx` |
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# React Guidelines
|
|
2
|
+
|
|
3
|
+
<!-- cairn preset: react. The frontend framework spine for a React app — Vite SPA
|
|
4
|
+
or the client layer of a Next.js app. Seeded 2026-09-09 from charityiq-app
|
|
5
|
+
(Sprint 0: CLAUDE.md conventions + ADR 0001, no component code yet). Single
|
|
6
|
+
consumer; refine when a second React repo appears.
|
|
7
|
+
|
|
8
|
+
What is NOT here: RSC, Server Actions, route handlers, the App Router. Those
|
|
9
|
+
are Next.js-specific — a `presets/nextjs` composes on top (`--stack
|
|
10
|
+
nextjs,drizzle`) if the repo stays Next.js. If it splits into a NestJS API +
|
|
11
|
+
a React SPA, this preset pairs with `presets/nestjs` (`--stack
|
|
12
|
+
nestjs,react,typeorm` on the API side is nonsense — each repo has its own
|
|
13
|
+
stack). The styling system is a separate add-on (`presets/tailwind`); this
|
|
14
|
+
preset is styling-agnostic. -->
|
|
15
|
+
|
|
16
|
+
## Components
|
|
17
|
+
|
|
18
|
+
* Function components only. One component per file, named after the file.
|
|
19
|
+
* Group by role: `components/ui/` (primitives), `components/feature/` (domain), `components/brand/` (marketing/identity).
|
|
20
|
+
* Props typed with an explicit `interface` or `type`; no `any` without a comment saying why.
|
|
21
|
+
* Derive, don't duplicate — compute from props/state in render, not in an effect.
|
|
22
|
+
|
|
23
|
+
## State
|
|
24
|
+
|
|
25
|
+
* **No `localStorage` or `sessionStorage`.** Persistent state lives server-side; per-session state lives in signed cookies. Client caches that survive a reload leak data across users on shared devices.
|
|
26
|
+
* Local UI state via `useState` / `useReducer`. Cross-tree state via context, kept small.
|
|
27
|
+
* Server state via a query library (or RSC) — not hand-rolled fetch-in-effect.
|
|
28
|
+
|
|
29
|
+
## Styling
|
|
30
|
+
|
|
31
|
+
* Framework-agnostic — the styling system is a composable add-on. For Tailwind, see
|
|
32
|
+
`presets/tailwind` (`--stack react,tailwind`). The one rule that holds regardless:
|
|
33
|
+
no hard-coded colours in components; go through whatever token layer the styling
|
|
34
|
+
preset defines.
|
|
35
|
+
|
|
36
|
+
## Tests
|
|
37
|
+
|
|
38
|
+
* Unit: **Vitest** + jsdom + Testing Library, in `tests/unit/`. Test behaviour, not implementation.
|
|
39
|
+
* HTTP mocks: **MSW**.
|
|
40
|
+
* End-to-end: **Playwright**, in `tests/e2e/`.
|
|
41
|
+
|
|
42
|
+
## Tooling
|
|
43
|
+
|
|
44
|
+
* TypeScript strict.
|
|
45
|
+
* **Biome** for formatting (tabs, double quotes) and import organisation; ESLint for lint rules Biome does not cover.
|
|
46
|
+
* Path alias `@/*` → repo root.
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
<!-- cairn preset: react — fills {{> stack/architect-discussion-topics }} (A.3). -->
|
|
2
|
+
- **Ambiguities** — anything in the description that admits multiple reasonable interpretations
|
|
3
|
+
- **UX / layout tradeoffs** — modal vs. page, tabs vs. stacked sections, inline vs. separate view
|
|
4
|
+
- **Component boundary** — extend an existing `ui/`/`feature/` component vs. add a new one
|
|
5
|
+
- **State placement** — local, context, or server
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
<!-- cairn preset: react — fills {{> stack/architect-references }} (A.1).
|
|
2
|
+
Primary-stack-only, not concatenated. -->
|
|
3
|
+
- Frontend source — components, hooks, the app shell
|
|
4
|
+
- The API's context docs, if the task touches a request/response shape the frontend consumes
|
|
5
|
+
- The API source, for shape confirmation
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
<!-- cairn preset: react — fills {{> stack/implementation-reference }} in the core
|
|
2
|
+
developer. -->
|
|
3
|
+
|
|
4
|
+
### Components
|
|
5
|
+
- Function component per file; props via an explicit `interface`
|
|
6
|
+
- Folder by role: `components/{ui,feature,brand}/`
|
|
7
|
+
- Compose small primitives; lift state only as far as it is shared
|
|
8
|
+
|
|
9
|
+
### Hooks
|
|
10
|
+
- Custom hooks in `hooks/` (or colocated), prefixed `use`
|
|
11
|
+
- One concern per hook; return a stable shape
|
|
12
|
+
- Effects only for genuine side effects (subscriptions, imperative DOM) — not to mirror props into state
|
|
13
|
+
|
|
14
|
+
### Styling
|
|
15
|
+
- Whatever the styling add-on defines (`presets/tailwind` for Tailwind repos)
|
|
16
|
+
- The constant: no inline hex in components, responsive and dark mode handled at the token layer
|
|
17
|
+
|
|
18
|
+
### Data
|
|
19
|
+
- Server state through a query library (or RSC where the framework provides it)
|
|
20
|
+
- No `localStorage` / `sessionStorage`
|
|
21
|
+
- Loading and error states are explicit, not implicit
|
|
22
|
+
|
|
23
|
+
### Tests
|
|
24
|
+
- `tests/unit/*.test.tsx` — Vitest + Testing Library, behaviour-first
|
|
25
|
+
- MSW for any HTTP the component makes
|
|
26
|
+
- `tests/e2e/*.spec.ts` — Playwright for the whole flow
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
<!-- cairn preset: react — fills {{> stack/key-patterns }} in the core architect
|
|
2
|
+
and developer. -->
|
|
3
|
+
|
|
4
|
+
Component and folder conventions, state rules, test wiring:
|
|
5
|
+
**`ai/infrastructure/code-guidelines.md`** § React.
|
|
6
|
+
|
|
7
|
+
Reusable primitives and domain components already built: **`components/`** — check
|
|
8
|
+
`ui/` and `feature/` for an analog before adding a new one.
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
<!-- cairn preset: react — fills {{> stack/plan-step-ordering }} in the core
|
|
2
|
+
architect's Plan format. Primary-stack-only, not concatenated. -->
|
|
3
|
+
1. Types / models / API client methods
|
|
4
|
+
2. Hooks and state
|
|
5
|
+
3. Components (`ui/` primitives first, then `feature/`)
|
|
6
|
+
4. Wiring — routes, layout, data loading
|
|
7
|
+
5. Styling (per the styling add-on) — responsive and dark-mode passes
|
|
8
|
+
6. Tests — Vitest units, then a Playwright flow
|
|
9
|
+
7. Context doc updates
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
<!-- cairn preset: react — fills {{> stack/review-checklist }} in the core reviewer.
|
|
2
|
+
Concatenated with any other stack entry's slot. -->
|
|
3
|
+
|
|
4
|
+
| Function components, one per file, typed props (no bare `any`) | component `.tsx` files |
|
|
5
|
+
| Component in the right folder — `ui/` primitive, `feature/` domain, `brand/` identity | `components/` |
|
|
6
|
+
| No `localStorage` / `sessionStorage` | any client code |
|
|
7
|
+
| No fetch-in-`useEffect` for server state — a query library or RSC | component / hook files |
|
|
8
|
+
| No hard-coded colours in components — go through the styling preset's token layer | `.tsx` |
|
|
9
|
+
| Derived values computed in render, not synced via an effect | component `.tsx` |
|
|
10
|
+
| New behaviour has a Vitest test; user-visible flow has a Playwright spec | `tests/unit/`, `tests/e2e/` |
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# Tailwind Guidelines
|
|
2
|
+
|
|
3
|
+
<!-- cairn preset: tailwind. The styling add-on — orthogonal to the framework, the
|
|
4
|
+
way an ORM add-on is. Compose it: `--stack nextjs,tailwind,drizzle`,
|
|
5
|
+
`--stack react,tailwind`. Seeded 2026-09-09 from charityiq-app (Tailwind v4).
|
|
6
|
+
Single consumer; refine when a second Tailwind repo appears.
|
|
7
|
+
|
|
8
|
+
Not in it: the repo's actual brand token values and font choice — those are
|
|
9
|
+
project-owned (charityiq's `app/globals.css` holds `coral`/`ink`/`paper` and
|
|
10
|
+
Geist). This preset says *where* tokens go and *how* to consume them. -->
|
|
11
|
+
|
|
12
|
+
## Tokens
|
|
13
|
+
|
|
14
|
+
* Design tokens live in a `@theme` block in the app's global stylesheet (Tailwind v4). **Never create a `tailwind.config.ts`** — Tailwind v4 ignores it.
|
|
15
|
+
* Semantic tokens (`--color-background`, `--color-foreground`) map to raw brand tokens; components use the semantic ones.
|
|
16
|
+
* Dark mode via `@media (prefers-color-scheme: dark)` re-mapping the semantic tokens — not a `dark:` variant on every element.
|
|
17
|
+
|
|
18
|
+
## Usage
|
|
19
|
+
|
|
20
|
+
* Consume tokens through the standard utility names Tailwind generates (`bg-coral`, `text-ink`, `font-sans`). No raw hex, `rgb()`, or CSS custom properties in component markup.
|
|
21
|
+
* Utility classes in the markup; reach for `@apply` only for a genuinely repeated cluster, in a component-scoped stylesheet.
|
|
22
|
+
* Responsive with Tailwind's breakpoint prefixes; do not hand-write media queries for layout.
|
|
23
|
+
* Class order left to the formatter (Biome / Prettier plugin) — do not hand-sort.
|
|
24
|
+
|
|
25
|
+
## Structure
|
|
26
|
+
|
|
27
|
+
* One global stylesheet for tokens and base layer; everything else is utilities in components.
|
|
28
|
+
* No global element selectors beyond a minimal base (`body`, `:root`).
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
<!-- cairn preset: tailwind — appended to the framework preset's
|
|
2
|
+
implementation-reference slot on `--stack <framework>,tailwind`. -->
|
|
3
|
+
|
|
4
|
+
### Styling
|
|
5
|
+
- Tailwind v4 utilities in the markup; tokens from the `@theme` block
|
|
6
|
+
- Semantic tokens (`background`/`foreground`) in components; raw brand tokens only in `@theme`
|
|
7
|
+
- Responsive via breakpoint prefixes (`md:`, `lg:`); dark mode via `prefers-color-scheme` token re-mapping
|
|
8
|
+
- No `tailwind.config.ts`, no inline hex, no per-element `dark:` sprawl
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
<!-- cairn preset: tailwind — appended to the framework preset's key-patterns slot
|
|
2
|
+
on `--stack <framework>,tailwind`. -->
|
|
3
|
+
|
|
4
|
+
Design tokens and the base layer: the app's global stylesheet `@theme` block.
|
|
5
|
+
Token names and dark-mode mapping conventions: **`ai/infrastructure/code-guidelines.md`** § Tailwind.
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
<!-- cairn preset: tailwind — appended to the framework preset's review-checklist
|
|
2
|
+
slot on `--stack <framework>,tailwind`. -->
|
|
3
|
+
|
|
4
|
+
| No raw hex / `rgb()` / CSS custom properties in markup — only token utilities (`bg-coral`, `text-ink`) | `.tsx` / `.html` |
|
|
5
|
+
| No `tailwind.config.ts` — tokens live in a `@theme` block in the global stylesheet | project root |
|
|
6
|
+
| Dark mode via re-mapped semantic tokens, not `dark:` on every element | global stylesheet |
|
|
7
|
+
| `@apply` only for a genuinely repeated cluster, component-scoped — not scattered | component stylesheets |
|
|
8
|
+
| No hand-written layout media queries — Tailwind breakpoint prefixes | `.tsx` / `.html` |
|