diff --git a/AGENTS.md b/AGENTS.md index 7bfabad76..d3259145a 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -7,21 +7,21 @@ - note: any formatting issue (such as tabs vs. spaces) can be resolved by running the `pnpm run biome:fix` command - typical way to structure pure logic is into Modules divided by logical domains which are imported with the "* as Module" import and then used like so "Module.foo()". These functions always need JSDoc. - non-exported functions typically do not need JSDoc or at least it can be kept short +- more topic docs live in `docs/dev/` — notably [architecture.md](./docs/dev/architecture.md) (feature folder layout) and [permissions.md](./docs/dev/permissions.md) (authorization: global roles via `requireRole()`/`useHasRole()`, per-object `permissions` computed in repositories) ## Commands +- `pnpm run checks` runs, in order: biome:fix, unit/browser tests, translation json checks, typecheck and knip — knip means unused exports fail checks, so remove exports nothing imports - `pnpm run typecheck` runs TypeScript type checking - `pnpm run biome:fix` runs Biome code formatter and linter - `pnpm run test:unit:browser` runs all unit tests and browser tests - `pnpm run test:e2e` runs all e2e tests - `pnpm run test:e2e:flaky-detect` runs all e2e tests and repeats each 10 times -- `pnpm run i18n:sync` syncs translation jsons with English +- `pnpm run i18n:sync` syncs translation jsons with English +- `pnpm run dev` starts the dev server (also runs migrations and setup first); `pnpm run seed` rebuilds the dev dataset. The dev admin user ("Sendou") has id 274 (`ADMIN_ID`) ## Typescript -- prefer early return over nesting if statements (bouncer pattern) -- do not use `any` type -- for constants use ALL_CAPS - always use named exports - Remeda is the utility library of choice - date-fns should be used for date related logic @@ -29,18 +29,19 @@ ## React -- prefer functional components over class components -- prefer using hooks over class lifecycle methods - do not use `useMemo`, `useCallback` unless it is to stabilize a `useEffect` dependency array value - state management is done via plain `useState` and React Context API - avoid using `useEffect` -- split bigger components into smaller ones -- one file can have many components - all texts should be provided translations via the i18next library's `useTranslations` hook's `t` function - instead of `&&` operator for conditional rendering, use the ternary operator -- fixed-field mutations (an `_action` plus hidden inputs) use `` which type checks the action and fields against the route's action schema; real multi-input forms instead pass `schema` alongside `_action` to `SubmitButton`; enforced by the `no-raw-action-forms` Biome plugin - for localized user-readable time strings use ``, `` or `useFormatDistanceToNow`. If needed use `useDateTimeFormat` directly. NEVER use e.g. `toLocaleString` directly as it does not include users' language selection. +## Forms + +- forms are built with the `SendouForm` schema-based system: a valibot schema using field builders from `~/form/fields` generates both the UI and server-side validation, see [forms.md](./docs/dev/forms.md) +- form label/help translations go in `locales/en/forms.json` +- fixed-field mutations (an `_action` plus hidden inputs) use `` which type checks the action and fields against the route's action schema; real multi-input forms instead pass `schema` alongside `_action` to `SubmitButton`; enforced by the `no-raw-action-forms` Biome plugin + ## Remix/React Router - new routes need to be added to `routes.ts` @@ -55,7 +56,6 @@ - use CSS modules - one file containing React code should have a matching CSS module file e.g. `Component.tsx` should have a file with the same root name i.e. `Component.module.css` -- clsx library is used for conditional class names - prefer using [CSS variables](./app/styles/vars.css) for theming - for any CSS variable used, make sure it is defined either locally or in the `vars.css` file - for simple styling, prefer [utility classes](./app/styles/utils.css) over creating a new class @@ -66,9 +66,9 @@ - database is Sqlite3, driven by Node's built-in `node:sqlite` through a custom Kysely dialect (`app/db/node-sqlite-dialect.ts`) - database code should only be written in Repository files, see [repositories.md](./docs/dev/repositories.md) for their conventions +- import `jsonArrayFrom`/`jsonObjectFrom`/`jsonBuildObject` from `~/utils/kysely.server`, never from `kysely/helpers/sqlite` (enforced by the `no-kysely-sqlite-helpers` Biome plugin); JSON columns are registered in `app/db/json-columns.ts` - migrations are Kysely migrations in `/migrations`, scaffolded with `pnpm run migrate:new "description"` and applied with `pnpm run migrate up`, see [how-to.md](./docs/dev/how-to.md) - down migrations are not needed, only up migrations -- every database id is of type number - if we are working on a branch by default we should add to the migration this branch added instead of creating a brand new one - `/app/db/tables.ts` contains all tables and columns available, see [database-schemas.md](./docs/dev/database-schemas.md) for how columns should be typed (booleans, timestamps, JSON, enums, SQLite migration quirks) - `db.sqlite3` is development database @@ -79,10 +79,10 @@ - library used for unit testing is Vitest - Vitest browser mode can be used to write tests for components -- use `test`, not `it` - name a test after the behaviour it establishes, with no `"should "` prefix (`test("returns null for an unknown id")`) - `describe` takes the bare function name, except for files consumed through a `* as Module` import, where it takes `Module.fn` — the way callers write it - when a test is `input -> expected output` with no setup, make it a `test.each` table rather than a run of near-identical `test` blocks; give every row a short label (`$why`, `%s`) so a failure names the case +- test and dev-seed setup writes go through factories, never raw repository/db calls (enforced by the `no-raw-db-writes-in-tests` and `no-raw-db-writes-in-dev-seed` Biome plugins). Factories are setup only: the operation under test is called through its repository directly, never through a factory - users come from `UserFactory.pool()` declared at module scope and filled in `beforeEach` — never a module-level `let` reassigned per test. Where positions carry meaning, name them with accessors next to the pool (`const actorId = () => users.id(1)`) - fixture builders shared by more than one test file live in a `tests/` folder of the feature they belong to (`app/features//**/tests/fixtures.ts`); don't copy a builder into a second file