# Development How to run Siftlode locally and pass the gate. Conventions for the backend code itself live in [`backend/app/README.md`](../backend/app/README.md). ## Node One major, everywhere. `.nvmrc` pins it; the `Dockerfile`'s frontend build stage, `frontend/package.json`'s `engines` and `@types/node` must all match it — and so must the machine you run the gate on. `frontend/src/lib/nodeEngine.contract.test.ts` fails the gate the moment any of them drift apart, including when yours is the odd one out. `nvm use` in the repo root picks it up. This is a contract, not a preference: tsc and vitest read the engine's own `Intl` and web APIs, and those change between majors — the same currency format renders `$160M` on one and `$160.0M` on the next. A gate that is green on a major you do not ship is green about nothing. ## Run it locally The whole stack (Postgres + API + worker) comes up from the base compose file plus the localdev override: ```bash docker compose -f docker-compose.yml -f docker-compose.localdev.yml up --build ``` Browse at **http://localhost:8080** (Google login works — the OAuth redirect is `http://localhost:8080/auth/callback`). The API runs `alembic upgrade head` on startup, so the schema is always current. For a fast frontend edit loop, run the Vite dev server with HMR against that API: ```bash cd frontend && npm install && npm run dev # http://localhost:5173, proxying the API on :8080 ``` The dev server also serves the **design lab** at `http://localhost:5173/design-lab`: the open design decisions and the chosen patterns, rendered with the app's real CSS. It never reaches a production build (see [`DESIGN.md`](DESIGN.md)). ## The gate A change is done when this is green, from the repo root: ```bash bash scripts/check.sh # every lane bash scripts/check.sh lint # the static lanes only (no tests, build or audits) — the quick loop bash scripts/check.sh --brief # either mode: one line per lane, output only for a red one bash scripts/lint-changed.sh # ruff + eslint + prettier on the uncommitted files only ``` It runs every lane even after one fails and names each red one at the end. `lint-changed.sh` is a convenience, not the gate; with `--hook` it doubles as a Claude Code Stop hook (silent when clean, the findings handed back otherwise). The codebase's health numbers — size, complexity, test ratio, gate weight, comment noise — come from `python scripts/audit_metrics.py` (seconds; it reports, it never fails). `--speed` adds the speed baseline (`frontend/e2e/speed.measure.ts`: the feed's first card and a shared `/watch` link's time-to-play against the :8080 stack, with `E2E_PASSWORD` set). An epic that changes structure compares its before and after. What it runs, and why each lane runs where it does, is in the script's comments; `frontend/src/lib/gates.contract.test.ts` holds its lines, so a lane dropped or narrowed there fails a test. What each gate deliberately does NOT check — and why — is the [gate register](gates.md). There is no CI: the gate runs here, and a release runs it (plus e2e and the CVE scan) before anything is merged. The lanes, for running one by hand: **Backend** ```bash # lint + format check (ruff is pinned in backend/requirements-dev.txt) python -m ruff check --config backend/ruff.toml backend scripts python -m ruff format --check --config backend/ruff.toml backend scripts # type check — in the test image (requirements.lock does not install on Windows); new errors only, # the pre-existing ones are in backend/.basedpyright/baseline.json docker compose -f backend/docker-compose.test.yml run --rm --no-deps --build test \ python -m basedpyright --project pyrightconfig.json # tests — pure-logic + the DB-backed integration lane, against an ephemeral Postgres, ending with the # coverage total. The backend tree is bind-mounted, so it always tests your working files. docker compose -f backend/docker-compose.test.yml up --build --abort-on-container-exit --exit-code-from test docker compose -f backend/docker-compose.test.yml down -v # the lock against the advisory databases, at any severity docker compose -f backend/docker-compose.test.yml run --rm --no-deps test \ pip-audit -r requirements.lock --no-deps --disable-pip ``` A missing database or ffmpeg is a hard error, not a skip — so the gate can't print green while the DB-backed tests were quietly not run (`SIFTLODE_TEST_REQUIRE_DB=0` is the explicit "pure-logic only" run for a machine without Postgres). Tests that need a database take the `db` fixture (`backend/tests/conftest.py`); the rest are pure-logic and run without one. **Frontend** (from `frontend/`) ```bash node ../scripts/check-node-modules.mjs # node_modules == package-lock.json (else: npm ci) npm run typecheck # tsc x3 (app / node / e2e) npm run lint # eslint (typed rules included; warnings fail) npm run format:check # prettier npm run knip # unused files/exports/deps — and unused by what SHIPS (--production) npm run test # vitest (incl. the compiler, size/complexity and gate-register contracts) npx vite build # the production bundle (devOnlyGuard fails it on dev-only code) npm audit --omit=dev --audit-level=high # what ships to the browser, at high or above ``` Coverage is reported, never enforced: `npm run test -- --coverage`, and the pytest lane's last line. The Playwright suite (`npm run e2e`) runs against the `:8080` stack built from your tree; the release requires it. Before an image is pushed, it is scanned with Trivy (HIGH/CRITICAL with a fix fails; exceptions in `.trivyignore.yaml`, each with a reason and an expiry). ## Branching Trunk-based: branch off `main`, keep the branch gate-green, and land the whole change back to `main` in one reviewed step. Migrations are Alembic revisions under `backend/alembic/versions`; the API applies them on startup, and the DB-lane rebuilds the schema via `alembic upgrade head`, so a broken migration chain fails the tests.