# AGENTS.md Guidance for AI agents (and a fast orientation for humans) working in this repo. ## What this is Aldine — a slim, self-hosted collaborative LaTeX platform (Overleaf alternative). Fastify server with embedded Hocuspocus/Yjs collab, per-project git repos (worktree per branch), sandboxed TeX Live compiler service, React 19 + Vite + CodeMirror 6 frontend. AGPL-3.0. ## Layout - `apps/server` — Fastify API, collab (Yjs snapshots + debounced git autocommit), auth (env-gated, off by default), datastore seam (`src/db/`: SQLite file default, JSON files via `ALDINE_DATASTORE=json`, Postgres via `DATABASE_URL`) - `apps/web` — React SPA; `src/editor/visual/` is the visual editing mode (pure decoration layer, **byte-stable by contract** — rendering must never change source bytes; an e2e test enforces this) - `apps/compiler` — zero-dependency Node HTTP wrapper around latexmk (restricted shell-escape, no egress in compose) - `e2e` — Playwright suites (`tests/`), screenshot/demo generators (`shots.mjs`, `demo.mjs`), fixtures - `deploy` — compose overlays, nginx sample, AWS (Terraform) + Hetzner demo stacks - `site` — aldine.dev landing page (GitHub Pages), `plugins`, `templates` ## Commands ```bash npm install # workspace root npm run typecheck -w apps/web # tsc, no emit npm run test -w apps/web # vitest unit tests npm run dev:server / dev:web / dev:compiler # local stack (:3000 / :5173 / :4020) npm run test:e2e # playwright (starts its own server) scripts/lifecycle/drill.sh upgrade|restore # Docker: upgrade / backup+restore drill (DATASTORE=postgres) ``` e2e needs the compiler on :4020 **with the e2e data dir**: `DATA_DIR=$(pwd)/.data-e2e PORT=4020 node apps/compiler/server.js` — a compiler on the default `.data` makes every compile test fail while looking healthy. `ALDINE_URL=http://localhost:8080 npm run test:e2e` targets a running compose stack instead. Two `07-features` tests need biblatex, which BasicTeX lacks; everything else passes locally. Two checkouts side by side (`reuseExistingServer` would otherwise test the other tree's server): `E2E_PORT=3101 E2E_MOCK_PORT=4920 E2E_GITLAB_PORT=4922 E2E_GITEA_PORT=4924 E2E_AUTH_PORT=3201 E2E_AUTH_MOCK_PORT=4928 E2E_AUTH_OIDC_PORT=4944 COMPILER_URL=http://localhost:4021 npm run test:e2e`. Every mock server runs with `reuseExistingServer`, so a port left at its default silently shares the other checkout's mock. CI (`ci.yml`, job `e2e`) runs every Playwright config on each pull request, one runner per suite: the main config in four `--shard`s, provisioning, auth, base path, and the remote smoke against a signed-in server under `/internal/aldine` that the job starts itself. The compiler is the published image at the docker-compose.yml pin (`latest` while a cut's version is unpublished) with this checkout's `apps/compiler/*.js` mounted over `/srv`, as the runner's uid, the suite's data dir at the same absolute path (`.github/actions/e2e-compiler`). Its scheme is medium, which has biblatex, so no suite needs the `-full` image. A failed suite uploads its HTML report and traces as the `playwright-` artifact. ## Conventions - Comments state constraints the code can't express — never narration of the change or the reviewer's rationale. - Every headline feature gets an e2e test; UI elements carry `data-testid`. - New/changed runtime deps: commit the updated `package-lock.json` or CI's `npm ci` fails. - Feature flags: experimental UI ships behind localStorage flags (`aldine.experimental.*`), announced in the command palette. - User-visible strings are sentence-case, concrete, and name the action. - `CHANGELOG.md` (Keep-a-Changelog) is updated with the feature, not after. - PR bodies say `Refs #N`, never `Closes`/`Fixes #N`: a closing keyword shuts the issue at merge time, days before a release exists, and the reporter gets no answer. Close issues by hand after the tag, with a comment that names the version and asks the reporter to confirm. ## Gotchas that have bitten before - **Byte stability** is the visual editor's core promise — any change there must keep the cursor-tour e2e green. - Yjs docs reload from binary snapshots (`META_DIR` sidecar); never reseed open docs from plain text — that duplicates content on reconnect. - With `DATABASE_URL` set, users, sessions, tokens, project metadata and comments live in Postgres, not in `META_DIR`: anything that copies "the data" (`deploy/backup.sh`, a migration) must cover both. On SQLite they are `META_DIR/aldine.db`, whose newest writes may sit in `aldine.db-wal`: copy a running one with `dist/db/snapshot.js`, never with `cp` or `tar`. The lifecycle drill (`.github/workflows/lifecycle.yml`) checks all three datastores. - `META_DIR` is deliberately outside `DATA_DIR` so the compiler container can never read API keys — don't "simplify" them onto one volume. - The file tree defaults to source-only view; e2e that asserts non-source files must toggle to "All" first. The open file and files the tab just created or uploaded show in it anyway, so an absence check needs a file added through the API. - Dark theme is the app default (stamped pre-paint in `index.html`); Playwright `colorScheme` emulation does *not* switch the app theme — drive `localStorage['aldine.theme']` instead. - git tokens are passed inline per operation, never written to `.git/config` (the compiler can read project dirs). - Playwright `webServer` timeouts are long on purpose; kill stray `tsx watch` processes if local ports hang. - Image tags: a version tag builds immutable `x.y.z` images and stops; `latest` moves only in `promote.yml` after a human approves. `build-image.yml` tags the index by hand for that reason; if you ever swap in `docker/metadata-action`, set `flavor: latest=false` or it re-adds `latest` for every release semver. `docker-compose.yml` pins `${ALDINE_VERSION:-x.y.z}` on purpose. Do not "simplify" either back to `latest`. - The `buildcache--` tags in the GHCR packages are the release layer cache, not images. Deleting one, or moving `build-image.yml` back to `type=gha`, makes every instance re-download TeX Live (1.4 GB) on the next release. OS package steps in the Dockerfiles go below `ARG PACKAGES_REFRESH`, or a cached layer freezes them without security fixes. - `test:unit` discovers `apps/server/test/*.test.mjs`; never enumerate test files in package.json again (two branches adding tests conflicted on that line and the merge silently dropped one side's tests).