AGENTS.md
Rama main · réplica de lectura del repositorio original.
# Agent Guidelines for Docs
This file contains guidelines for AI agents coding in this repository.
## Project Overview
**La Suite Docs** is a collaborative text editor built by DINUM (French government) and ZenDiS (German government). It features real-time editing, offline support, AI actions, and multi-format export (PDF/DOCX/ODT).
Repository: https://github.com/suitenumerique/docs
## Monorepo Structure
- `src/backend/` - Django REST API (Python 3.14+)
- `src/frontend/apps/impress/` - Main Next.js application (TypeScript, pages router, static export)
- `src/frontend/apps/e2e/` - Playwright E2E tests
- `src/frontend/packages/i18n/` - Shared i18n utilities
- `src/frontend/packages/eslint-plugin-docs/` - Custom ESLint plugin
- `src/frontend/servers/y-provider/` - Conversion service only (Express, `POST /api/convert/`). It no longer serves any websocket
- `src/yhub-server/` - Collaboration server: a thin TypeScript wrapper around `@y/hub` (websockets, REST routes, worker). It holds the content of the documents
- `src/mail/` - Email templates (MJML)
- `src/helm/` - Helmfile and Helm chart: see `src/helm/README.md` (local Kind cluster, services, guidelines)
- `src/loadtest/` - Load generators (k6, swarm, canary): see `documentation/load-testing.md`, never against production
- `documentation/` - Documentation for operators and contributors
## Development Commands
### Setup and Services
```bash
make bootstrap # Full dev setup (build + migrate + demo + run)
make build # Build all Docker containers
make run # Start all services
make stop # Stop services
make build-yhub # Build the collaboration server (yhub) image
make migrate-yhub # Create/upgrade the yhub database schema (`yarn init-db`), safe to re-run
make status # Check running services
```
### Backend (Python/Django)
Tests run inside Docker containers. You must first build the backend image and ensure the `lasuite-network` Docker network exists before running tests.
```bash
make build-backend # Build the backend Docker image (required before first test run)
docker network create lasuite-network # Create the external network (required once)
bin/pytest -n auto # Run all backend tests in parallel
bin/pytest -n auto path/to/test # Run specific test file/directory
bin/pytest -n auto path/to/test.py::TestClass::test_method # Run single test via docker compose
make lint # ruff format + ruff check + pylint
make lint-ruff-format # Format only
make lint-ruff-check # Lint only
make migrate # Run database migrations
make makemigrations # Create new migrations
make resetdb # Flush DB + create superuser (admin/admin)
```
### Frontend (TypeScript/Next.js)
```bash
# From src/frontend/apps/impress/:
yarn dev # Development server (port 3000)
yarn build # Production build (includes prettier + stylelint checks)
yarn lint # TypeScript check + ESLint
yarn test # Run Vitest tests
yarn prettier # Format code
yarn stylelint # Lint CSS
# From project root:
make frontend-lint # Lint all frontend workspaces
make frontend-test # Run frontend tests
```
### Collaboration server (yhub, TypeScript)
```bash
# From src/yhub-server/ (standalone package: its own package.json and yarn.lock,
# not a workspace of src/frontend):
yarn test # Vitest unit tests (@y/hub is mocked, no store needed)
yarn typecheck # tsc --noEmit
yarn lint # ESLint
yarn build # Compile to dist/
yarn init-db # Create/upgrade the yhub postgres schema (what `make migrate-yhub` runs)
```
When the host has no `yarn`, or `node_modules` is root-owned (created by the compose
bind mount, a host install fails with EACCES), run the suite inside the running dev
container, from the project root: `docker compose exec yhub yarn test`.
The container starts with `node --import ./dist/sentry.js dist/server.js`: keep the
`--import` when overriding the command. `src/yhub-server/README.md` is the reference
for everything this server does (permissions, roles, migration, storage, metrics).
### Without yarn on the host
Yarn and Node on the host are optional, except Node for E2E tests: when
`command -v yarn` fails, run the same command in the dev image of the package
(`frontend-development`: apps/impress, `yhub`: src/yhub-server,
`y-provider-development-converter`: servers/y-provider), with
`docker compose exec <service> yarn …` when it runs, otherwise
`DOCKER_USER=$(id -u):$(id -g) docker compose run --rm --no-deps <service> yarn …`.
Details: `docs-frontend` and `docs-e2e-test` skills, "Tests" in
`src/yhub-server/README.md`.
### Mails
```bash
make mails-install # Install dependencies
make mails-build # Convert MJML to HTML + plaintext
```
### i18n
```bash
make i18n-generate # Extract translation strings (backend + frontend)
make i18n-compile # Compile translations for all apps
```
### Dev Service URLs
- Frontend: http://localhost:3000
- Backend API/Admin: http://localhost:8071
- Keycloak (auth): http://localhost:8083
- MinIO (S3): http://localhost:9000
- Collaboration server (yhub): http://localhost:3002 (websocket on `/collaboration/ws/v1/docs/{docid}`)
- yhub PostgreSQL: localhost:5433 (its own database, apart from the backend's on 15432)
- Mailcatcher: http://localhost:1081
## Architecture
### Backend
- **Framework**: Django + DRF, configured via `django-configurations` (`src/backend/impress/settings.py`)
- **Main app**: `src/backend/core/` (models, API viewsets, services, authentication)
- **API**: REST on `/api/v1.0/` with nested routes (e.g., `/documents/{id}/accesses/`)
- **Auth**: OIDC via `mozilla-django-oidc` (Keycloak in dev)
- **Background tasks**: Celery + Redis
- **Database**: PostgreSQL 16 with `django-treebeard` for document hierarchy
- **Storage**: S3-compatible (MinIO in dev) for attachments and media. **The content of a document is not stored by the backend anymore**: it lives in the collaboration server (yhub). `Document.content`, `save_content` and the S3 object versions are gone; `file_key` only survives as a pointer for the migration of legacy documents
- **Collaboration server client**: `core/services/yhub_services.py` (`YHubService`: `get_ydoc`, `create_ydoc`, `delete_ydoc`, `restore_ydoc`, `reset_ydoc`, `migrate`, `reset_connections`). It is called synchronously by document creation from a file, `duplicate`, `formatted-content`, and from Celery tasks for delete/restore/access changes. Mock it in tests (`CELERY_TASK_ALWAYS_EAGER=True` in the `Test` configuration, so tasks run inline)
- **Service-to-service auth**: short-lived RS256 JWTs with an `aud` claim, never a shared bearer token. The backend signs with `JWT_PRIVATE_KEY` (`core/services/jwt_services.py`) and publishes its keys on `/api/v1.0/jwks`; yhub signs with `YHUB_JWT_PRIVATE_KEY` and publishes `/collaboration/jwks/v1`. Dev keys are generated in `data/jwt/` by `make bootstrap`
- **Removed endpoints**: `documents/{id}/content/` (GET and PATCH), `documents/{id}/versions/`, `documents/{id}/can-edit/`. New ones: `documents/{id}/content-updated/` (called by yhub) and `documents/{id}/accesses/me/`. `documents/{id}/formatted-content/` now reads the content from yhub
- **Legacy documents**: `python manage.py migrate_documents` hands the S3 content of existing documents to yhub (resumable, idempotent); `SOFT_MIGRATION=true` on yhub seeds a document on first open. See `UPGRADE.md`
- **Search**: Full-text indexing via `manage.py index`
- **Load-test tooling**: `src/backend/loadtest/` is a separate Django application, only installed by the `LoadTest` configuration (a subclass of `Production`); `LOAD_TEST_TOOLS_ENABLED` is pinned to `False` in `Production` and never read from the environment. Anything added for load tests goes there, not in `core`, and must stay off in `Production`
### Frontend
- **Editor**: BlockNote.js (Tiptap-based block editor; check the version pinned in `package.json`, its API changes between minor versions)
- **Real-time**: Yjs with the plain `y-websocket` `WebsocketProvider` (`features/docs/doc-management/stores/useProviderStore.tsx`), and an HTTP polling fallback (`@y/yhub-http-fallback`) used while the websocket cannot be opened. Hocuspocus is gone
- **Offline**: documents are kept locally (`y-indexeddb`) and a service worker (`features/service-worker/`) caches pages and API answers and replays queued mutations when back online
- **Version history**: built from the activity API of yhub, not from the backend
- **State**: Zustand (broadcast store for cross-tab sync), TanStack React Query for server state
- **UI**: `@gouvfr-lasuite/ui-components` + Cunningham design system, Mantine for BlockNote
- **Export**: BlockNote XL packages (PDF, DOCX, ODT)
- **i18n**: i18next with Crowdin for community translations
### Collaboration Flow
Clients connect via WebSocket to the collaboration server (`src/yhub-server`, built on `@y/hub`), authenticated by their Django session cookie.
- On every connection (and permission recheck, REST call, fallback poll) yhub asks the backend who the caller is and what they may do: `GET /users/me/`, `GET /documents/{id}/abilities/`, and `GET /documents/{id}/accesses/me/` for the history. The backend stays the source of truth for access rights
- Updates travel between replicas through Redis/Valkey streams, so yhub replicas are stateless and need no sticky sessions. A worker (`YHUB_ROLE=all|server|worker`) compacts the stream into yhub's own PostgreSQL database (optionally S3 for the blobs)
- After a compaction that found new content, yhub calls `POST /documents/{id}/content-updated/` so that the backend updates `updated_at` and the search index
- The backend reads and writes content through the REST routes of yhub with an admin JWT (see `YHubService`)
## Observability
- **Metrics**: opt-in Prometheus endpoints on the backend and yhub, guarded by a required `PROMETHEUS_API_KEY` (`documentation/metrics.md`, "Metrics" in `src/yhub-server/README.md`). Never put a document id, a user id or a path in a metric label
- **Sentry**: backend (`SENTRY_DSN`), y-provider and yhub (`src/yhub-server/src/sentry.ts`)
- **Profiling**: django-silk, opt-in with `SILK_ENABLED` (`documentation/profiling.md`)
## Code Style Guidelines
### Python (Backend)
- **Formatter**: Ruff (line-length: 88)
- **Linter**: Ruff + Pylint (pylint runs only on files changed from `origin/main`)
- Imports (Ruff enforced): future, stdlib, django, third-party, impress (`core`), first-party, local
- Naming: PascalCase classes, snake_case functions/variables, UPPER_SNAKE_CASE constants
- **Print statements are forbidden** (T20 rule) - CI will reject them in backend code
- Use `@transaction.atomic`, `select_related`/`prefetch_related`, type hints `list[str]`
- Run migrations after model changes
- In `settings.py`, `post_setup` runs after django-configurations has handed the settings to Django: change lists and dicts **in place** there (`append`, `insert`, item assignment), a reassignment is silently ignored
- Use specific exceptions, no unused imports, double quotes, f-strings
- **Tests**: Prefer the `settings` fixture from `pytest-django` over `django.test.override_settings`
### TypeScript/React (Frontend)
- **Formatter**: Prettier (single quotes, trailing commas, semicolons, 80 char width)
- **Linter**: ESLint 9 with custom `eslint-plugin-docs`
- **Style**: Stylelint for CSS
- Imports: React & Next.js, libraries, internal, types, styles
- Naming: PascalCase components/types, camelCase functions, `use*` hooks, UPPER_SNAKE_CASE constants
- Strict mode, interfaces for shapes, Zustand for state, React Query for data
### TypeScript (yhub-server)
- Every environment variable is read, validated and refused at import time in `src/config.ts` (secrets through `secret()` in `src/env.ts`, which supports `NAME_FILE`): add new settings there, with a test in `__tests__/config.spec.ts`
- Prettier style with single quotes; tests are Vitest and mock `@y/hub`
- `@y/y` must stay the very copy `@y/hub` resolves (`__tests__/dependencies.spec.ts`)
### Helm
- Never commit secrets; document every value with an `@param` annotation and regenerate the chart README (`src/helm/impress/README.md`)
## Git Conventions
### Commit Format
```
<gitmoji>(type) lowercase title
Mandatory description explaining why.
```
- **No space** between emoji and `(type)`, **one space** after closing parenthesis
- Types: `backend`, `frontend`, `ci`, `docker`, `dependencies`, `e2e`, `export`, `auth`, etc.
- Gitmoji examples: `✨` feature, `🐛` fix, `♻️` refactor, `⬆️` dependency upgrade, `🔒️` security
- Commits must be signed (`-S`) and signed-off (`--signoff`)
- One commit = one concern, and the tests and docs of a change go in the same commit, never in a follow-up one (see `CONTRIBUTING.md`)
### Changelog
Update `CHANGELOG.md` under `[Unreleased]` with format: `- <gitmoji>(type) description` (max 80 chars per line).
### PR Checklist
Run before submitting: `make lint && make frontend-lint && make test` (without yarn
on the host, see the `docs-frontend` skill)
## General Practices
- Use DRF for REST APIs with proper serializers
- Implement proper error handling with try/except blocks
- Use Django settings for configuration
- Internationalize user-facing strings with `_()` (backend) or i18next `t()` (frontend, always with a `description` for the translator); never edit `translations.json`, it is pulled from Crowdin
- Cache expensive operations, use database indexes
- Write docstrings for classes and complex functions
- Keep the comments in the code you add short and concise: explain the why, not the what
## Environment Configuration
Config files in `env.d/development/`: `common`, `postgresql`, `kc_postgresql`, `crowdin`, `yhub`, `yhub-postgres`. The collaboration server reads its own two files and none of the backend's. Create `.local` overrides (gitignored) for custom settings.
## Working efficiently
- Load the Skill that matches the task before exploring: it already holds the commands, helpers and pitfalls.
- Big files: grep for the symbol, then read only that range. `core/api/viewsets.py`, `core/models.py` and `core/api/serializers.py` are never worth reading whole. Backend tests are split per endpoint and action (`core/tests/documents/test_api_documents_<action>.py`), so glob for the test file.
- Never read generated or vendored files: `translations.json` (i18n), `yarn.lock`, `CHANGELOG.md` beyond its `[Unreleased]` section, `node_modules/`, `dist/`, `.next/`.
- Verify narrowly first and widen only once that passes: `bin/pytest -n auto <file>::<test> -x -q --tb=short` or `yarn vitest run <file>`, and lint only the files you touched (`yarn eslint <files>`, `docker compose run --rm app-dev ruff check <files>`, with paths relative to `src/backend/`); without yarn on the host, run the yarn ones in Docker (see "Without yarn on the host"). Run the full `make lint` / suites once, at the end.
- Filter long command output (`| tail`, `grep`, `-q`) rather than reading it all.
## Incidental findings
While on a task, if you come across something **unrelated to it** that clearly deserves fixing (a real bug, a security issue, or a strong improvement: algorithm or SQL efficiency, documentation, CI), do not fix it or widen the task. At the end, tell the user in one or two lines (what, where, why it matters) and offer to file it with the `docs-create-issue` Skill, labelled `AI generated`. Only report what you verified, not style nits or guesses.
## Knowledge feedback
At the end of a meaningful task, consider whether you verified something repository-specific that would have saved real time or prevented a mistake had you known it at the start: a required service, a command that looks right but is not, a test-suite limitation, a helper to prefer, an invariant, a recurring failure and how to diagnose it.
Record it only if all of these hold:
- it is verified, not a guess;
- it is not already findable in this file, a Skill, a README, the Makefile, a package script or the nearby code;
- a competent developer or agent doing a similar task six months from now would be materially helped by it.
Never record facts about the current issue, temporary bugs, line numbers, branch names, one-off decisions or generic programming advice. Do not update agent documentation just to summarize the task.
Put it in the narrowest place that fits:
- **This file**: repository-wide rules and conventions that most tasks need, whatever the agent
- **An existing Skill** for the workflow it belongs to (backend, frontend, E2E tests, review, running the stack…), when your agent setup has one. Skills are versioned in `.agents/skills/<name>/SKILL.md`, the one place to edit them: `.claude/` is gitignored and `make skills` regenerates `.claude/skills/` from it, so an edit there is lost and will not show in `git diff`. The same goes for the `local-*` skills, which only exist in `.claude/skills/`: name them in your report
- **The subsystem's own documentation** (e.g. `src/yhub-server/README.md`, `documentation/`) when it is useful to humans too
- **A new Skill** only for a substantial, repeatable workflow with no natural home, never for a single tip
A repository fact belongs in the repository, where every agent and developer gets it, not in a personal agent memory; keep personal memory for the user's own preferences.
Prefer amending an existing line or section to adding a new one, and keep it to a line or two. Give the reason when it is not obvious. An instruction you found to be wrong is worth fixing or removing too. When in doubt, do not edit: mention it in your report instead. A human reviews the change like any code.
This file is loaded into every session, so each line costs tokens on every task. Before adding a line here, check whether it only serves one area; if so, it belongs in that area's Skill or README. Remove what has gone stale or become derivable from the code. Two Skills that trigger on the same request (a similar `description`) must be merged.
Close your report with one short line, and skip it for trivial tasks:
```text
Agent knowledge: none.
Agent knowledge: added to <file> that <fact>, because <what it would have saved>.
Agent knowledge (unverified, not recorded): <fact>.
```