Imported from ValentinLepiller/nf525-form (
AGENTS.md). Install upstream withnpx skills add ValentinLepiller/nf525-form. Copyright stays with the author.
nf525-form — Repository Guidelines
Stack
- pnpm monorepo: product apps in
apps/, shared libraries inpackages/. TypeScript everywhere; JS is acceptable only for tooling/config files. - Shared contracts: Zod schemas in
packages/contracts, consumed by every app. - Utils: es-toolkit. Dates: date-fns. Validation: Zod.
- Lint/format: oxlint + oxfmt via the root scripts.
Commands
Run from the repo root: pnpm dev, pnpm build, pnpm lint, pnpm format, pnpm typecheck, pnpm test. Per-app commands are documented in each app's package.json.
Coding Style
- Formatting and linting come from the root oxfmt/oxlint config; never add per-app style overrides.
- Prefer type-only imports (
import type { … }). - React components: PascalCase. One feature per folder on the frontend (
features/<feature>/). - Code identifiers and comments are written in English, even when the product/UI is in another language.
Code Comments
- Minimal comments; prefer self-documenting names. Only comment the non-obvious: why, edge cases, workarounds.
- Describe the current behaviour, not where it came from. Comparisons with previous implementations belong in the PR description, not the source.
- No multi-line explanatory blocks; keep any necessary comment to a single concise line.
Validation & Schemas
- Every DTO/input/output has a Zod schema defined in
packages/contracts; types are inferred from schemas, never hand-written in parallel. - Never duplicate a schema inside an app. If an app needs a variant, derive it (
.pick,.omit,.extend) from the shared one. - A pure business rule used by more than one app lives in a shared package, not copied into apps.
- When removing a business field, search all apps/packages/tests/migrations for both camelCase and snake_case spellings.
Verification Before Handoff
- After changing workspace dependencies or package exports, run
pnpm installbefore trusting typecheck/test results. - For cross-package changes, run targeted typechecks on every affected workspace (
pnpm --filter <pkg> typecheck), not just the app you edited. - After editing
packages/contracts, build it (pnpm --filter @nf525-form/contracts build) before a filtered typecheck of a dependent app — apps resolve the built output, so a filteredpnpm --filter <app> typechecksees stale exports otherwise. Rootpnpm typecheckhandles the ordering via turbo. - Run
pnpm lint,pnpm format:checkandpnpm typecheckbefore claiming a branch is ready; run the focused tests covering what you changed. - Include
git diff --checkin the final pass; review your own full diff before handing off.
Commits & Pull Requests
- Conventional Commits: single-line message, lowercase type, imperative description, no trailing period, no body, no Co-Authored-By.
- Commits are atomic: one logical change per commit (a migration, a schema, an endpoint, a page). A feature lands as a series of small commits, each passing lint and typecheck on its own — never as one feature-sized commit. Plumbing fixes discovered mid-feature get their own
fix:/chore:commit, not a ride-along. - Branch names:
<type>/<short-kebab-description>(e.g.feat/store-access-report). - PR title follows the same convention as commits. PR description contains: what changed and why, scope summary, linked issue/ticket, screenshots or GIFs for any UI change, and explicit callouts for env-var, migration, or breaking changes.
- Before opening a PR, self-review: re-read the entire diff and remove leftovers — debug logs, commented-out code, unused imports, unrelated formatting churn, files outside the task's scope. If the diff contains changes you cannot justify from the task, revert them.
- Quality gate: lint, typecheck, and the relevant test suites pass locally before the PR is opened. Never push directly to the default branch.
Security & Configuration
- Never commit plaintext secrets. The local
.envstays uncommitted; staging/prod env files are committed encrypted with Galacrypt (.env.*.galacrypt, declared in.galacryptrc.json). The.galacryptkeycomes from Passbolt and must never be committed. - Never run anything against a production database or environment without an explicit request from a human; state it clearly when a command targets prod.
- Respect the Node/pnpm versions pinned in
.nvmrc/package.json#engines.
Module Conventions
Frontend SPA (Vite + TanStack Router)
- Routing uses TanStack Router with file-based routes: one file per route in
src/routes/(createFileRoute); runpnpm --filter web generate-routesafter adding routes (dev/build/typecheck do it automatically). Never editrouteTree.gen.tsby hand. Query params go through the router's validated search params (Zod), never hand-parsed fromwindow.location. - Server state goes through TanStack Query; do not mirror it into a client store. Client stores are for UI state only.
- API calls live in
src/lib/(or a per-feature data layer) using the shared Axios instance, and every response is parsed with the contracts schemas — components never call HTTP clients directly. Error toasts surface the server's message viaapiErrorMessagefromsrc/lib/api.ts. - UI primitives are shadcn/ui, vendored under
src/components/ui/(button, input, label, dialog, select, table, popover, calendar and a composed date-picker are pre-installed). New primitives come from the CLI:pnpm dlx shadcn@latest add <component>— check the shadcn registry before hand-writing anything inui/; hand-write only when no shadcn equivalent exists. - Feature code composes primitives from
src/components/ui/and styles with theme tokens (bg-background,text-muted-foreground,text-destructive, …), never raw palette classes liketext-neutral-500. Never importradix-uioutsidesrc/components/ui/. - Styling structure: global CSS only for imports, theme tokens and base rules; Tailwind utilities in components for layout, responsive behaviour and states; shared components or
cvavariants for reused styles. Edit existing theme definitions instead of appending overrides. - Dates shown to users go through
formatDate/formatDateTimefromsrc/lib/dates.ts; date selection uses the sharedDatePicker(src/components/ui/date-picker.tsx). Never format dates ad hoc withtoLocaleDateStringor raw ISO slicing. - Toasts use Sonner (
toast.success(…),toast.error(…)); the<Toaster />is mounted once at the root. - Debounce/throttle/rate limiting uses TanStack Pacer; do not hand-roll timers for these.
Forms
- Forms use
useAppFormfromsrc/lib/form.tsx(TanStack Form composition):<form.AppField name="…">{(field) => <field.InputField label="…" />}</form.AppField>and<form.AppForm><form.SubmitButton label="…" /></form.AppForm>. Do not wire fields inline with rawuseForm. - Reusable field types are registered once in
fieldComponentsinsrc/lib/form.tsx(built on the form-field wrapper + shadcn primitives); any field pattern used twice gets promoted there. Split large forms withwithForm, never by copy-paste. - Validation reuses the Zod schemas from
packages/contracts— never duplicate validation rules inside a component. Submit errors surface through Sonner toasts. - Date fields use
<field.DateField label="…" />, which binds an ISOyyyy-MM-ddstring: type themz.iso.date()in the contracts schema (optional:z.iso.date().nullish().transform((v) => v || null)) with''as the default value. Convert to aDate/PrismaDateTimein the API service, not in the form. - Optional text fields shared with the API use
.nullish().transform((v) => v || null)in the contracts schema (not.nullable().transform, which breaks TanStack Form's validator typing), withdefaultValuestyped asz.input<typeof schema>so empty-string defaults typecheck. - A form's validator schema must cover the exact shape of the whole form values object: never pass a narrower schema or a union of schemas as a validator. When a form only edits part of a contracts schema (or switches modes), derive one schema per form with
.pick/.omit/.extendwhose input type equalsdefaultValues. - Never install react-hook-form or formik, and never run
shadcn add form(it is react-hook-form based).