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.
Before your first PR — the checklist
Section titled “Before your first PR — the checklist”- 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-bytrailers. - Named imports, never namespace imports. No
voidon promises. - Logic in
src/lib/server/api/; routes stay thin. Server env via$env/dynamic/private. - Ran the Svelte autofixer on any
.sveltefile you touched. - Used design tokens and literal Tailwind classes only.
- Ran the local gate (
pnpm check,pnpm test:unit,pnpm buildfromapps/web) and, if it’s a UI change, did a quick manual pass. - Did not
git pushwithout explicit permission.
Code style
Section titled “Code style”- 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', neverReact.useState. - Don’t
voida promise to quiet a linter — call it directly. - Server logic belongs in
src/lib/server/api/. A+server.tsroute should be a thin wrapper (~40 lines: parse, validate, delegate, serialise). If you’re writing business logic in a route, move it. $env/dynamic/privatefor 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
.sveltefile before finalising; use the MCP’slist-sections/get-documentationfor Svelte 5 questions rather than guessing.
The toolchain: vp
Section titled “The toolchain: vp”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.
Test-driven development
Section titled “Test-driven development”Red–green, two commits, every behavioural change:
- 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). - 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.
Testing
Section titled “Testing”Unit tests (Vitest via vp test). ~194 test files, co-located with their source (foo.ts → foo.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.tsexposescreateTestDb()— a Miniflare D1 with every migration applied, wrapped in Drizzle — plusinsertUser/insertGroupand 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.tsfactory with mock deps — no DOM, no network. (search-controller.svelte.test.tsdrives out-of-order resolution through a fake fetcher;preview-state.svelte.test.tsinjects fakedecode/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.varsmust contain bothDEV_SKIP_AUTH=trueand a strong 32+ charJWT_SECRET.DEV_SKIP_AUTHalone fails: Wrangler emulates thebrightblur.apphost, which forces realvalidateEnvand 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:migratefirst, 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.
Commits and changesets
Section titled “Commits and changesets”- Conventional commits:
type(scope): summary—feat,fix,refactor,test,perf,chore,docs. Scope optional but preferred (e.g.fix(publish): …). - No co-authors. Do not add
Co-authored-bytrailers, regardless of any tool’s default. - Changesets are
patchunless told otherwise. - Never
git pushwithout explicit permission. Branch first if you’re onmain.
The merge gate
Section titled “The merge gate”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:
- Run the exact CI commands locally on the head commit:
pnpm check,pnpm test:unit,pnpm build, all fromapps/web. - Get an independent review of the PR.
- Record the local-gate substitution in a PR comment (what you ran, on which SHA, with the results).
- If the PR includes a migration, run
cd apps/web && pnpm db:migrate:remotemanually after the deploy lands (Workers Builds deploys code, but the deadmigrate-prodjob won’t apply the schema — code against a stale schema 500s). Verify withwrangler d1 migrations list brightblur --remote --env production.
Revert to hard CI gating when Actions minutes return.
The design system
Section titled “The design system”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.