form-ruleslisted
Install: claude install-skill g-bastianelli/nuthouse
# subroutine — form discipline
Apply this to a form's schema and the components that bind it. The nearest
`AGENTS.md` picks the form, validation, design-system and i18n libraries and
wins; the APIs below assume Zod >= 4.1 and React Hook Form. A schema no form
binds is out of scope.
## Convert in the schema, nowhere else
- An `<input>` holds a string: convert in a codec, never in the form library
(`valueAsNumber`, `setValueAs`) or by hand. Two converters disagree on what
empty means — `valueAsNumber` yields `NaN`, which `z.number()` rejects with an
untranslated developer string.
- Split the codec: the input schema owns the shape, so `decode` stays a bare
`Number`; the exported value schema owns the range, so `-1` is told to be
positive instead of unreadable.
- Blank, malformed and out-of-range are three failures with three messages, and
`abort` after blank. Use the library's format constants, never a copied regex.
- Mirror the contract's bounds: a form looser than its API turns an inline
message into a failed request.
- Route messages through the repo i18n as thunks (`{ error: () => m.key() }`) —
a plain string in a module-scope schema freezes the locale active at import.
- A closed option set (Select, Switch, radio) parses nothing: convert in the
control's `onChange`.
```ts
// `abort` matters: without it a blank field fails both checks and reports
// "invalid" for a value the user never entered.
const slaveId = z.codec(
z
.string()
.trim()