Skip to content

Conventions & Process

Read this before your first pull request. None of it is exotic, but the project is consistent about it and reviews enforce it.

  • Red–green TDD, two commits. Failing test first (red), implementation second (green).
  • British English, correct diacritics, em-dashes with hair spaces — in code, comments, UI copy, and docs.
  • Conventional commit messages; no Co-authored-by trailers.
  • Named imports, never namespace imports. No void on promises.
  • Logic in src/lib/server/api/; routes stay thin. Server env via $env/dynamic/private.
  • Ran the Svelte autofixer on any .svelte file you touched.
  • Used design tokens and literal Tailwind classes only.
  • Ran the local gate (pnpm check, pnpm test:unit, pnpm build from apps/web) and, if it’s a UI change, did a quick manual pass.
  • Did not git push without explicit permission.
  • British English everywhere, with full orthographic correctness — keep diacritics (café, naïve, Umeyama), never ASCII-fold them. In prose, typeset em-dashes with a hair space (U+200A) either side: , not --.
  • Named imports only: import { useState } from 'react', never React.useState.
  • Don’t void a promise to quiet a linter — call it directly.
  • Server logic belongs in src/lib/server/api/. A +server.ts route should be a thin wrapper (~40 lines: parse, validate, delegate, serialise). If you’re writing business logic in a route, move it.
  • $env/dynamic/private for server env vars, never $env/static/private.
  • Tailwind: literal class strings only (no variable-composed class names), no arbitrary values, no raw colours — use the design tokens. See Frontend Architecture and docs/design.md.
  • Svelte: run the Svelte MCP autofixer on every .svelte file before finalising; use the MCP’s list-sections / get-documentation for Svelte 5 questions rather than guessing.

The repo runs on Vite+. Use vp (wrapped by the pnpm scripts) for dev/build/test/lint/format — don’t invoke pnpm/npm/npx directly for those tasks, and import test utilities from vite-plus/test, not vitest. AGENTS.md has the full Vite+ notes.

Red–green, two commits, every behavioural change:

  1. Write the test that captures the desired behaviour or reproduces the bug. Run it; watch it fail for the right reason. Commit it (test(scope): … (red) style).
  2. Implement the smallest change that makes it pass. Commit (fix/feat).

The point is that a reviewer can check out the red commit and see the bug demonstrated, then check out the green commit and see it fixed. Single-commit “here’s the feature and its tests” PRs don’t pass review.

Unit tests (Vitest via vp test). ~194 test files, co-located with their source (foo.tsfoo.test.ts). vite.config.ts holds the config; src/lib/test-setup.ts is the only global setup (it polyfills ImageData for Node).

  • Server API tests run against a real in-memory D1. src/lib/server/api/test-helpers.ts exposes createTestDb() — a Miniflare D1 with every migration applied, wrapped in Drizzle — plus insertUser/insertGroup and named DIDs (ALICE, BOB, CHARLIE). So API tests exercise actual SQLite semantics, including .batch() atomicity.

    const { db, client, dispose } = await createTestDb();
    await insertUser(client, ALICE);
    const result = await someApiFunction(db, ALICE, /* … */);
    expect(result).toEqual(/* … */);
    await dispose();
  • State-module tests call the .svelte.ts factory with mock deps — no DOM, no network. (search-controller.svelte.test.ts drives out-of-order resolution through a fake fetcher; preview-state.svelte.test.ts injects fake decode/renderUrl.)

End-to-end tests (Playwright). ~51 specs in apps/web/e2e/, two projects: chromium (fast, 60 s) and chromium-heavy (240 s — the upload/model/batch/publish specs). The web server is wrangler dev on port 4173 (not Vite preview — Wrangler emulates the Cloudflare bindings). To run it:

  • e2e/.dev.vars must contain both DEV_SKIP_AUTH=true and a strong 32+ char JWT_SECRET. DEV_SKIP_AUTH alone fails: Wrangler emulates the brightblur.app host, which forces real validateEnv and a JWT-entropy check, so every request 500s and Playwright times out.
  • Port 4173 is hardcoded with reuseExistingServer: !CI — make sure nothing else is serving 4173, or you’ll silently test the wrong build.
  • A fresh worktree needs pnpm db:migrate first, or global-setup fails with “no such table: sessions”.

The TFLite regression can only be caught by the heavy e2e. tflite-runtime.ts / worker-bootstrap changes are unreachable from Vitest (the importScripts polyfill is gated off Node), so they require pnpm build && npx playwright test upload.test.ts --project=chromium-heavy. See Architecture and Recognition.

  • Conventional commits: type(scope): summaryfeat, fix, refactor, test, perf, chore, docs. Scope optional but preferred (e.g. fix(publish): …).
  • No co-authors. Do not add Co-authored-by trailers, regardless of any tool’s default.
  • Changesets are patch unless told otherwise.
  • Never git push without explicit permission. Branch first if you’re on main.

Normally CI (.github/workflows/ci.yml) runs check + test:unit + build and applies prod migrations. Right now GitHub Actions minutes are exhausted, so CI does not run. Until they return, the interim policy is:

  1. Run the exact CI commands locally on the head commit: pnpm check, pnpm test:unit, pnpm build, all from apps/web.
  2. Get an independent review of the PR.
  3. Record the local-gate substitution in a PR comment (what you ran, on which SHA, with the results).
  4. If the PR includes a migration, run cd apps/web && pnpm db:migrate:remote manually after the deploy lands (Workers Builds deploys code, but the dead migrate-prod job won’t apply the schema — code against a stale schema 500s). Verify with wrangler d1 migrations list brightblur --remote --env production.

Revert to hard CI gating when Actions minutes return.

Visual decisions — colour tokens, type scale, spacing, radii, component primitives — have a single source of truth in docs/design.md at the repo root (mirrored into @theme in apps/web/src/routes/layout.css). Read it before creating or modifying UI, and when you introduce a new token, update docs/design.md, layout.css, and sweep the usages in the same change.