Skip to content

Commit 27ae60c

Browse files
authored
feat: utilisation de react-hook-form avec la validation zod (#3024)
1 parent 27e420b commit 27ae60c

54 files changed

Lines changed: 1507 additions & 806 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.claude/rules/automation.md

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,15 @@ Each audit agent scopes itself based on the files actually modified.
5555
5656
---
5757

58+
## Feature lifecycle (mandatory)
59+
60+
### At the END of every feature
61+
62+
After completing the implementation, run `/verify-feature`.
63+
The skill loops until zero violations — **never report a task as done with known issues**.
64+
65+
---
66+
5867
## Automatic quality gates (mandatory)
5968

6069
These gates trigger **automatically** without user input. Do NOT wait to be asked.
@@ -91,7 +100,7 @@ Full checklist (13 RGAA themes) in `.claude/agents/rgaa-auditor/AGENT.md`.
91100

92101
Verify **inline while writing** AND audit all created/modified files after implementation:
93102
- Queries → Drizzle ORM only (no raw SQL)
94-
- tRPC inputs → Zod schemas (in `schemas.ts`, not inline)
103+
- tRPC inputs → Zod schemas from `~/modules/{domain}/schemas.ts` (never inline, never in routers)
95104
- Protected routes → `protectedProcedure`
96105
- Mutations → ownership check (`userId` from session)
97106
- Multi-write → `db.transaction()`
@@ -136,5 +145,6 @@ Skills in `.claude/skills/` can be triggered explicitly with `/command`:
136145
| `/audit-secu` | Deep OWASP + RGS audit with detailed report |
137146
| `/create-page` | Create pages from Figma (4-phase parallelized workflow) |
138147
| `/process-issue` | Process a GitHub issue end-to-end with mandatory RGAA + security gates |
148+
| `/verify-feature` | Full rules audit (forms, schemas, DRY, a11y, security) — loops until zero issues |
139149

140150
These produce detailed reports and are more thorough than the automatic inline gates.

.claude/rules/code-quality.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,13 @@ Keep files under 200 lines. Split at 400. Files over 800 lines are forbidden.
7777
- **Error handling**: always `try/catch` with explicit user-facing error message
7878
- **Input validation**: Zod at system boundaries (forms, route params, API body)
7979

80+
## Form conventions
81+
82+
- **No manual form state**: never use multiple `useState` for form fields. Use `useZodForm` from `~/modules/shared`.
83+
- **No inline validation**: never write manual `if (!field) { setError(...) }` in handleSubmit. Use Zod schemas via `zodResolver`.
84+
- **No router-level schemas**: never define Zod schemas in `src/server/api/routers/`. Define them in `src/modules/{domain}/schemas.ts` and import from there.
85+
- **Shared schemas**: the same Zod schema must be used by both the form (`useZodForm`) and the tRPC procedure (`.input()`).
86+
8087
## Environment variables
8188

8289
Declared and validated in `src/env.js` via `@t3-oss/env-nextjs` + Zod. **Never read `process.env` directly** — always `import { env } from "~/env.js"`.

.claude/rules/trpc-api.md

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -5,30 +5,31 @@ paths:
55

66
# tRPC API
77

8-
## Zod schemas in dedicated files
8+
## Zod schemas in module folders (shared frontend/backend)
99

10-
Define Zod input/output schemas in a `schemas.ts` file next to the router, not inline in procedure definitions.
11-
This enables reuse across procedures and in client-side form validation.
10+
Zod schemas live in `src/modules/{domain}/schemas.ts` **never** in `src/server/api/routers/`.
11+
Routers import schemas from modules. Forms use the same schemas via `useZodForm`.
1212

1313
```ts
14-
// FORBIDDEN — inline schema
14+
// FORBIDDEN — inline schema in router
1515
export const myRouter = createTRPCRouter({
1616
create: protectedProcedure
1717
.input(z.object({ siren: z.string(), year: z.number() }))
1818
.mutation(...)
1919
});
2020

21-
// CORRECT — schemas.ts
22-
// schemas.ts
23-
export const createInput = z.object({ siren: z.string(), year: z.number() });
24-
25-
// router.ts
21+
// FORBIDDEN — schema in src/server/api/routers/schemas.ts
2622
import { createInput } from "./schemas";
23+
24+
// CORRECT — schema in src/modules/{domain}/schemas.ts
25+
import { createInput } from "~/modules/myDomain/schemas";
2726
export const myRouter = createTRPCRouter({
2827
create: protectedProcedure.input(createInput).mutation(...)
2928
});
3029
```
3130

31+
Router files must **never** `import { z } from "zod"` — all Zod usage is in module schema files.
32+
3233
## TRPCError with proper codes
3334

3435
Always throw `TRPCError` (not plain `Error`) with the appropriate HTTP-semantic code:
Lines changed: 192 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,192 @@
1+
---
2+
name: verify-feature
3+
description: Full codebase rules audit — runs at END of every feature, loops until zero issues
4+
---
5+
6+
# /verify-feature
7+
8+
Comprehensive audit of ALL project rules. **Loops automatically** — fix every issue found, re-audit, repeat until zero issues.
9+
10+
## When this runs
11+
12+
- **Automatically at the END of every feature** (before reporting done — see `automation.md § Feature lifecycle`)
13+
- **Manually** via `/verify-feature` when you want a full check
14+
15+
## Instructions
16+
17+
### Phase 1 — Structural audit (read-only, do it yourself)
18+
19+
Detect changed files:
20+
```bash
21+
git diff --name-only HEAD # uncommitted changes
22+
```
23+
24+
If no uncommitted changes, use `git diff origin/master...HEAD --name-only` scoped to `packages/app/src/`.
25+
26+
Run ALL checks below on the changed files. If a check is not relevant to the changed files, mark it `SKIP` and move on.
27+
28+
---
29+
30+
#### 1.1 Forms — react-hook-form everywhere (`code-quality.md § Form conventions`)
31+
32+
For every `.tsx` file with `<form` or `useMutation`:
33+
- `useState` for form field data without `useZodForm`**VIOLATION**
34+
- Manual `e.preventDefault()` in `handleSubmit` without `form.handleSubmit`**VIOLATION** (exception: parameterless confirmation mutations)
35+
- Dual state: `useState` duplicating data already in `useZodForm`**VIOLATION**
36+
- `useState` for UI-only state (saved, errors, modals) → OK
37+
38+
#### 1.2 Schemas — Zod in the right places (`code-quality.md § Form conventions`, `trpc-api.md`)
39+
40+
```bash
41+
# Must return ZERO — no Zod in routers
42+
grep -rn "from ['\"]zod['\"]" src/server/api/routers/ --include="*.ts"
43+
44+
# Must return ZERO — no Zod in components
45+
grep -rn "from ['\"]zod['\"]" src/modules/ --include="*.tsx"
46+
47+
# Must return ZERO — no inline z.object in API routes
48+
grep -rn "z\.object(" src/app/api/ --include="*.ts"
49+
```
50+
51+
#### 1.3 Schema quality (`code-quality.md § DRY`)
52+
53+
- No two schemas defining the same shape across files
54+
- No dead exports (types/schemas exported but never imported)
55+
- Every `modules/*/schemas.ts` re-exported from its `modules/*/index.ts` barrel
56+
57+
#### 1.4 File size (`code-quality.md § File size`)
58+
59+
```bash
60+
# Flag files over 400 lines (split required) — BLOCK files over 800
61+
wc -l $(git diff --name-only HEAD -- '*.ts' '*.tsx') 2>/dev/null | sort -rn | head -20
62+
```
63+
64+
#### 1.5 Imports (`code-quality.md § Imports`)
65+
66+
```bash
67+
# Must return ZERO — no deep relative imports
68+
grep -rn "from ['\"]\.\.\/\.\.\/" src/modules/ --include="*.ts" --include="*.tsx"
69+
```
70+
71+
#### 1.6 No custom components in src/app/ (`code-quality.md § No custom components`)
72+
73+
```bash
74+
# Must return ZERO — only route files allowed
75+
find src/app -name "*.tsx" ! -name "page.tsx" ! -name "layout.tsx" ! -name "loading.tsx" ! -name "error.tsx" ! -name "not-found.tsx" ! -name "global-error.tsx" ! -name "template.tsx" ! -name "default.tsx" ! -name "opengraph-image.tsx" ! -path "*/__tests__/*" | head -20
76+
```
77+
78+
#### 1.7 TypeScript (`code-quality.md § TypeScript`)
79+
80+
```bash
81+
# Must return ZERO — no explicit any (excluding tests)
82+
grep -rn ": any\b\|as any\b" src/modules/ src/server/ --include="*.ts" --include="*.tsx" | grep -v "__tests__" | grep -v "\.test\."
83+
```
84+
85+
#### 1.8 Environment variables (`code-quality.md § Environment variables`)
86+
87+
```bash
88+
# Must return ZERO — no direct process.env (excluding allowed files)
89+
grep -rn "process\.env" src/ --include="*.ts" --include="*.tsx" | grep -v "env.js" | grep -v "instrumentation.ts" | grep -v "next.config" | grep -v "trpc/react.tsx"
90+
```
91+
92+
#### 1.9 Database (`database-drizzle.md`)
93+
94+
For changed files in `src/server/`:
95+
- Multi-write without `db.transaction()`**VIOLATION**
96+
- `new Date()` at module scope → **VIOLATION**
97+
98+
#### 1.10 React components (`react-components.md`)
99+
100+
For changed `.tsx` files:
101+
- Inline `<svg>`**VIOLATION** (blocked by hook)
102+
- Raw `<img>`**VIOLATION** (blocked by hook)
103+
- `.map()` callback over 5 lines of JSX → **VIOLATION**
104+
105+
#### 1.11 Styling (`styling-dsfr.md`)
106+
107+
For changed `.scss` and `.tsx` files:
108+
- Raw `@media` with width/screen → **VIOLATION**
109+
- Hardcoded hex/rgb colors → **VIOLATION**
110+
- `style={` inline → **VIOLATION** (blocked by hook)
111+
112+
#### 1.12 Testing (`testing.md`)
113+
114+
For changed files:
115+
- New component/function without corresponding test → **WARNING**
116+
- Test mocks duplicating `src/test/setup.ts` mocks → **VIOLATION**
117+
- New page without E2E test → **WARNING**
118+
119+
#### 1.13 tRPC & Security (`trpc-api.md`, `automation.md § Gate 3`)
120+
121+
For changed files in `src/server/`:
122+
- tRPC input without schema from `~/modules/{domain}/schemas`**VIOLATION**
123+
- Router file importing `z` from `zod`**VIOLATION**
124+
- Mutation without ownership check → **WARNING**
125+
- Multi-write without `db.transaction()`**VIOLATION**
126+
- Raw SQL → **VIOLATION**
127+
128+
#### 1.14 Accessibility (`automation.md § Gate 2`)
129+
130+
For changed `.tsx` files:
131+
- `<input>` without associated `<label>`**VIOLATION**
132+
- `target="_blank"` without `<NewTabNotice />`**VIOLATION**
133+
- Decorative icon without `aria-hidden="true"`**WARNING**
134+
- Heading hierarchy skipping levels → **VIOLATION**
135+
- Form groups without `<fieldset>` + `<legend>`**WARNING**
136+
137+
---
138+
139+
### Phase 2 — Quality gates (matches `automation.md § Automatic quality gates`)
140+
141+
Launch **3 parallel agents** for validation:
142+
143+
1. **Agent: typecheck**`pnpm typecheck`
144+
2. **Agent: tests**`pnpm test`
145+
3. **Agent: lint+format**`pnpm lint:check && pnpm format:check`
146+
147+
Then launch **2 parallel agents** for audits (scoped to changed files):
148+
149+
4. **Agent: RGAA** — delegate to `rgaa-auditor` agent (`.claude/agents/rgaa-auditor/AGENT.md`) on all changed `.tsx` files. If no `.tsx` files changed → `SKIP`.
150+
5. **Agent: Security** — delegate to `security-auditor` agent (`.claude/agents/security-auditor/AGENT.md`) on all changed `.ts/.tsx` files in `server/`, `routers/`, or tRPC. If none → `SKIP`.
151+
152+
---
153+
154+
### Phase 3 — Fix loop
155+
156+
If **any VIOLATION** was found in Phase 1 or Phase 2:
157+
158+
1. Fix all violations
159+
2. Go back to **Phase 1 step 1** — re-run the full audit
160+
3. Repeat until **zero violations** across both phases
161+
162+
**Never report completion with known violations.**
163+
164+
---
165+
166+
### Phase 4 — Final report
167+
168+
Only after zero violations remain:
169+
170+
```
171+
## Feature Verification: PASS ✅
172+
173+
### Rules checked
174+
| Rule | Files | Status |
175+
|---|---|---|
176+
| Forms (react-hook-form) | X files | PASS |
177+
| Schemas (no inline Zod) | X routers | PASS |
178+
| File size (< 400 lines) | X files | PASS |
179+
| Imports (no deep relative) | X files | PASS |
180+
| TypeScript (no any) | X files | PASS |
181+
| Env vars (no process.env) | X files | PASS |
182+
| ... | ... | ... |
183+
184+
### Quality gates
185+
| Gate | Status |
186+
|---|---|
187+
| Typecheck | clean |
188+
| Tests | X/X passed |
189+
| Lint + Format | clean |
190+
| RGAA | PASS / SKIP |
191+
| Security | PASS / SKIP |
192+
```

.github/CODEOWNERS

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
1-
# Protect workflow files
2-
.github/workflows/*.yml @socialgouv/sre
3-
.github/CODEOWNERS @socialgouv/sre
4-
.kontinuous/ @socialgouv/sre
1+
# Default reviewers for all PRs
2+
* @SocialGouv/admins-egapro
53

6-
@gary-van-woerkens
4+
# Protect workflow files
5+
.github/workflows/*.yml @socialgouv/sre @SocialGouv/admins-egapro
6+
.github/CODEOWNERS @socialgouv/sre @SocialGouv/admins-egapro
7+
.kontinuous/ @socialgouv/sre @SocialGouv/admins-egapro

CLAUDE.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,7 @@ When creating multiple pages/screens, follow this 4-phase approach:
140140
| `/audit-secu` | Deep OWASP + RGS security audit with detailed report + auto-fix |
141141
| `/create-page` | Create pages from Figma (4-phase parallelized workflow) |
142142
| `/process-issue` | Process a GitHub issue (parent + sub-issues) with mandatory quality gates |
143+
| `/verify-feature` | Full rules audit — runs auto at END of every feature, loops until zero issues |
143144

144145
---
145146

packages/app/CLAUDE.md

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -234,6 +234,50 @@ Never add redundant `role` on semantic elements (`role="navigation"` on `<nav>`
234234

235235
---
236236

237+
## Forms
238+
239+
### Form state management
240+
241+
All forms must use `react-hook-form` with Zod validation via the shared `useZodForm` hook:
242+
243+
```tsx
244+
import { useZodForm } from "~/modules/shared";
245+
import { mySchema } from "~/modules/{domain}/schemas";
246+
247+
const form = useZodForm(mySchema, { defaultValues: { ... } });
248+
```
249+
250+
**Forbidden patterns:**
251+
- Multiple `useState` calls for form fields — use `useZodForm` instead
252+
- Manual imperative validation in `handleSubmit` — Zod handles it
253+
- Inline Zod schemas in tRPC routers — extract to `modules/{domain}/schemas.ts`
254+
255+
### Shared validation schemas
256+
257+
Zod schemas are the **single source of truth** for both frontend forms and tRPC backend:
258+
259+
```
260+
src/modules/{domain}/schemas.ts <- define schemas here
261+
src/modules/{domain}/index.ts <- re-export from barrel
262+
src/server/api/routers/{x}.ts <- import from ~/modules/{domain}/schemas
263+
src/modules/{domain}/MyForm.tsx <- import from ~/modules/{domain}/schemas
264+
```
265+
266+
Never define schemas in `src/server/api/routers/`. Always in `src/modules/{domain}/schemas.ts`.
267+
268+
### DSFR form integration
269+
270+
- `register()` spreads directly on native `<input>` elements with DSFR classes
271+
- Use `Controller` for non-standard controls (radios, custom selects)
272+
- Field errors: `fr-input-group--error` + `<p className="fr-error-text">`
273+
- Form errors: `fr-alert fr-alert--error` with `aria-live="polite"`
274+
275+
### File uploads
276+
277+
File upload forms keep the `useFileUploadForm` hook pattern. They do not need `react-hook-form` since the file upload lifecycle (select, validate, upload to S3, save metadata via tRPC) is distinct from standard form submission.
278+
279+
---
280+
237281
## File size
238282

239283
< 200 lines ideal, 200-400 acceptable, > 400 split, > 800 **forbidden**.

packages/app/package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
"@auth/drizzle-adapter": "^1.11.1",
3535
"@aws-sdk/client-s3": "^3.1006.0",
3636
"@gouvfr/dsfr": "^1.14.3",
37+
"@hookform/resolvers": "^5.2.2",
3738
"@react-pdf/renderer": "^4.3.2",
3839
"@sentry/nextjs": "^10.39.0",
3940
"@socialgouv/matomo-next": "^1.11.0",
@@ -49,6 +50,7 @@
4950
"postgres": "^3.4.8",
5051
"react": "^19.2.4",
5152
"react-dom": "^19.2.4",
53+
"react-hook-form": "^7.71.2",
5254
"server-only": "^0.0.1",
5355
"superjson": "^2.2.6",
5456
"zod": "^4.3.6"

packages/app/src/app/api/export/download/route.ts

Lines changed: 2 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,7 @@
1-
import { z } from "zod";
2-
31
import { downloadExport } from "~/modules/export/downloadExport";
2+
import { exportYearQuerySchema } from "~/modules/export/schemas";
43
import { db } from "~/server/db";
54

6-
const querySchema = z.object({
7-
year: z
8-
.string()
9-
.regex(/^\d{4}$/, "Year must be YYYY format")
10-
.transform(Number),
11-
});
12-
135
/**
146
* GET /api/export/download?year=2026
157
*
@@ -18,7 +10,7 @@ const querySchema = z.object({
1810
export async function GET(request: Request) {
1911
try {
2012
const url = new URL(request.url);
21-
const parsed = querySchema.safeParse({
13+
const parsed = exportYearQuerySchema.safeParse({
2214
year: url.searchParams.get("year") ?? undefined,
2315
});
2416

0 commit comments

Comments
 (0)