diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..dde1be0 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,27 @@ +# Contributing + +## Setup + +```bash +npm install # also installs git hooks (lefthook) via the prepare script +``` + +## Before you commit + +Hooks run automatically (format + lint on commit, typecheck on push). To run the full gate +yourself: + +```bash +npm run ci # format:check + lint + typecheck (client+server) + check:server + tests + build +``` + +## Standards + +All conventions — formatting, error handling, API/wire format, security, logging, testing — +live in **[docs/CODE_STANDARDS.md](docs/CODE_STANDARDS.md)**. Follow the canonical pattern for +each; new code must pass the CI gate. + +## Commits + +Conventional-commit style (`feat:`, `fix:`, `chore:`, `refactor:`, `ci:`, `style:`, `test:`). +Keep mechanical reformatting in its own commit. diff --git a/docs/CODE_STANDARDS.md b/docs/CODE_STANDARDS.md new file mode 100644 index 0000000..8faef05 --- /dev/null +++ b/docs/CODE_STANDARDS.md @@ -0,0 +1,77 @@ +# Code Standards + +The living rulebook for this codebase. One canonical way to do each thing, enforced in +CI so it can't drift. Sections marked **(enforced)** fail CI; **(target)** are being +rolled out. Update this doc when a standard changes. + +## Tooling & enforcement (enforced) + +- **Formatting:** Prettier (`.prettierrc.json`) — single quotes, semicolons, 2-space, + `printWidth: 100`, trailing commas. Run `npm run format`; CI runs `format:check`. +- **Lint:** `npm run lint` (`eslint .`) covers **client and server**. Focused rule set + (react-hooks on the client; `js/recommended` + TS-unused on the server). Errors block CI; + warnings are advisory (ratchet down over time). +- **Typecheck:** `npm run typecheck` (client, strict + `noUncheckedIndexedAccess`) and + `npm run typecheck:server` (server). Both run in CI. +- **Local hooks (lefthook):** pre-commit auto-formats staged files + lints; pre-push + typechecks. Installed by `npm install` (the `prepare` script). +- **Dead code:** `npm run knip` (advisory) for unused exports/deps. +- **Full gate:** `npm run ci` = format:check + lint + typecheck (×2) + check:server + + tests (server + client) + build. The CI pipeline runs the same. + +## Modules & files + +- Server is **CommonJS TypeScript `.cts`**, run by Node via type-stripping (no build step). + Every migrated module is `require`d **with the explicit `.cts`/`.mts` extension** + (extensionless does not resolve). Each `.cts` leads with an `import type …` line so + type-stripping activates. +- Client is ESM `.ts`/`.tsx` with the `@/*` path alias. +- Money is **integer cents** in the DB and on the wire; convert to dollars only at the edge + via the money helpers. Branded `Cents`/`Dollars` types where practical. + +## HTTP API conventions + +- **Wire format:** JSON, **snake_case** field names (mirrors DB columns), money as integer + cents. +- **Error responses (target):** every error body is `{ error, message, code, field? }` with + the correct HTTP status. Throw the factories from `utils/apiError.cts` + (`ValidationError`, `NotFoundError`, `ConflictError`, …) and let the single terminal error + handler format them — do not hand-roll `res.status().json({ error })`. Express 5 forwards + rejected async handlers automatically, so route bodies should not wrap everything in + `try/catch`. Clients read `message`/`code` + status. +- **Success envelope (target):** reads return the bare resource/list; mutations return + `{ success: true, … }`. +- **Validation (target):** validate inputs with the shared validators (throwing + `ValidationError`); do not scatter ad-hoc `parseInt`/range checks. + +## Security (target; see also SECURITY.md) + +- Every **mutating** route carries CSRF + auth + an appropriate rate-limit + input + validation. Admin routes require `requireAdmin`. +- Every user-scoped query filters `user_id` **and** `deleted_at IS NULL`. Never interpolate + user input into SQL — use bound parameters. +- Never log secrets, tokens, or raw financial values. +- Fail fast at boot on missing required config (e.g. `TOKEN_ENCRYPTION_KEY` in production). + +## Logging (target) + +- One `log` helper with levels; env-driven level; redaction of secrets/PII. Replace raw + `console.*` over time. + +## Data integrity + +- Every **multi-statement write** runs inside `db.transaction()`. +- Migrations are append-only versioned entries in `db/migrations/versionedMigrations.cts` + (idempotent; guard column adds with `PRAGMA table_info`). + +## Testing + +- Server: `node:test` in `tests/*.test.js`. Client: Vitest (`*.test.ts[x]`). E2E: Playwright + (`npm run test:e2e`, probe subset `test:e2e:probe`). Add coverage for new handlers; prove + money invariants with property tests where practical. + +## Future / modernization (tracked, not yet adopted) + +Kysely (typed SQL) · Zod (validation) · pino (logging) · native `--env-file` + validated +config · `node:sqlite` · ts-rest + OpenAPI · Vite 6/7. Adopt where each makes the code more +uniform; see the standardization plan.