chore: add CLAUDE.md (#32801)
* chore: add CLAUDE.md * chore: pr feedback * chore: contributing guideline rules * chore: pr feedback (cherry picked from commit fc30140e6ec313c94405e7ca4fbab8096dc1c6c7)
This commit is contained in:
@@ -0,0 +1,21 @@
|
|||||||
|
# AGENTS.md
|
||||||
|
|
||||||
|
Immich monorepo: `server/` (NestJS API + job workers), `web/` (SvelteKit SPA), `mobile/` (Flutter), `native/` (Rust core for mobile), `machine-learning/` (FastAPI + ONNX), `e2e/` (API + Playwright tests), `packages/` (`@immich/sdk`, `@immich/cli`, plugin SDK/core, release scripts), `open-api/`, `i18n/`, `docs/`. Before changing code in `server/`, `web/`, `mobile/`, `native/`, `machine-learning/` or `e2e/`, read that folder's `AGENTS.md`.
|
||||||
|
|
||||||
|
## Rules
|
||||||
|
|
||||||
|
- No PRs of LLM-generated code; the human contributor must understand and be able to explain every change (see `CONTRIBUTING.md`).
|
||||||
|
- Do not generate PR titles, descriptions, issue text, review comments or discussion answers. Translating human-written text is fine.
|
||||||
|
- Never work on issues labelled `good first issue`; those PRs are closed automatically.
|
||||||
|
- Run every build, lint, test and codegen task through mise: `mise //<dir>:<task>`, root tasks as `mise //:<task>`. `mise tasks ls --all` lists them.
|
||||||
|
- Never hand-edit generated files: `open-api/immich-openapi-specs.json`, `packages/sdk/src/fetch-client.ts`, `mobile/generated/openapi/`, `server/src/queries/*.sql`, and Dart codegen (`*.g.dart`, `*.drift.dart`, `*.freezed.dart`, `router.gr.dart`, `lib/generated/`, `lib/platform/*.g.dart`).
|
||||||
|
- Only edit `i18n/en.json`; the other locale files come from Weblate.
|
||||||
|
- Do not add code comments. Make the code self-explanatory through naming and structure instead.
|
||||||
|
- PR titles are conventional commits. Keep a PR to one focused change.
|
||||||
|
- Sharing/asset ownership and external libraries are feature-frozen: bug fixes only.
|
||||||
|
- Renaming or removing a docs page requires an entry in `docs/static/_redirects`.
|
||||||
|
|
||||||
|
## Skills
|
||||||
|
|
||||||
|
- **Regenerate the API clients**: after any change to a server controller or DTO, run `mise //:open-api`. It rebuilds the server, rewrites the OpenAPI spec, regenerates the TypeScript SDK and the Dart client. The CI job "OpenAPI Clients" fails if this is stale. If a response field became required, the Dart client also needs a backward-compatibility patch in `open-api/patch`, checked by `flutter test test/openapi_patches_coverage.dart` (job "check-mobile-patches").
|
||||||
|
- **Add a translation string**: add the key to `i18n/en.json`, then run `mise //:i18n:format-fix` so the file stays key-sorted (CI job "Test i18n" fails otherwise). For the mobile app also run `mise //mobile:translation`. Web keys are typed against `en.json`, so the key must exist before `tsc` passes.
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
# e2e/AGENTS.md
|
||||||
|
|
||||||
|
End-to-end tests against a real Immich stack from `docker-compose.yml` (server on port 2285): API specs with vitest, browser specs with Playwright.
|
||||||
|
|
||||||
|
## Rules
|
||||||
|
|
||||||
|
- `test-assets/` is a git submodule; initialize it before running exif or upload specs.
|
||||||
|
- Use the shared helpers in `src/utils.ts` (`utils.adminSetup`, `utils.userSetup`, `utils.resetDatabase`, `utils.createAsset`, `utils.waitForQueueFinish`, `asBearerAuth`, `asKeyAuth`, `immichCli`) instead of re-implementing login or fixtures.
|
||||||
|
|
||||||
|
## Skills
|
||||||
|
|
||||||
|
- **Run API specs**: `src/specs/server/**/*.e2e-spec.ts`, run with `mise //e2e:test` (it builds the SDK, CLI and Docker images first). If nothing answers at `127.0.0.1:2285`, the vitest `globalSetup` starts the Docker stack itself; set `VITEST_DISABLE_DOCKER_SETUP=true` to reuse a running one. One file: `pnpm exec vitest --run src/specs/server/api/album.e2e-spec.ts`. Maintenance-mode specs use `pnpm exec vitest --run --config vitest.maintenance.config.ts`.
|
||||||
|
- **Run browser specs**: Playwright projects are `web` (`src/specs/web/`, real backend), `maintenance` (`src/specs/maintenance/web/`), and `ui` (`src/ui/specs/`, mocked network from `src/ui/mock-network/`). `mise //e2e:test-web` runs all projects and installs browsers first. One project or file: `pnpm exec playwright test --project=web src/specs/web/album.e2e-spec.ts`; add `--ui` for the Playwright UI. Set `PLAYWRIGHT_DISABLE_WEBSERVER=1` to reuse a running stack.
|
||||||
|
- **Write an API spec**: in `beforeAll` call `await utils.resetDatabase()` and `admin = await utils.adminSetup()`, create users with `utils.userSetup(admin.accessToken, dto)`, and call the server through `@immich/sdk` functions with `asBearerAuth(token)` or through `supertest` on `app` for raw status-code and validation checks. After uploads, `await utils.waitForQueueFinish(admin.accessToken, 'thumbnailGeneration')` before asserting on derived data.
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
# machine-learning/AGENTS.md
|
||||||
|
|
||||||
|
Python 3.12 FastAPI service running ONNX models for CLIP, facial recognition and OCR, managed with uv.
|
||||||
|
|
||||||
|
## Rules
|
||||||
|
|
||||||
|
- Install with `uv sync --extra cpu`; run tools through `uv run` or the mise tasks.
|
||||||
|
- `mise //machine-learning:check` is `mypy --strict`, and ruff enforces line length 120. Code must pass both.
|
||||||
|
- `immich_ml/main.py` is the app (`/predict`, `/ping`); `pipeline.py` composes the Clip, FacialRecognition and Ocr pipelines.
|
||||||
|
|
||||||
|
## Skills
|
||||||
|
|
||||||
|
- **Add or change a model**: models live in `immich_ml/models/{clip,facial_recognition,ocr}/` and extend `InferenceModel` in `models/base.py`, which downloads weights with Hugging Face `snapshot_download` into the cache folder. Loaded models are kept in `models/cache.py` `ModelCache`, keyed by class, name and graph, with TTL eviction. Execution backends (ONNX Runtime, RKNN, Arm NN) are in `immich_ml/sessions/`.
|
||||||
|
- **Run tests**: everything is in `test_main.py`. One class with `uv run pytest test_main.py -k TestCache`, one test with `test_main.py::TestCLIP::test_name`. `mise //machine-learning:test-full` downloads real models and runs the prediction endpoint tests.
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
# mobile/AGENTS.md
|
||||||
|
|
||||||
|
Flutter app with Riverpod, Drift (SQLite), auto_route, pigeon and a Rust core in `../native/`.
|
||||||
|
|
||||||
|
## Rules
|
||||||
|
|
||||||
|
- Run `mise //mobile:checkout` after switching branches. All generated Dart is git-ignored; never edit it.
|
||||||
|
- New code follows `lib/domain/` (models, services, utils; no dependency on presentation or infrastructure) → `lib/infrastructure/repositories/` → `lib/presentation/`, wired by providers in `lib/providers/`. New data access goes in `lib/data/` (`lib/data/db/main/` holds the Drift database, `dao/`, `query/`, `table/`; `lib/data/server/` the API base).
|
||||||
|
- Top-level `lib/pages`, `lib/widgets`, `lib/services`, `lib/repositories`, `lib/entities` are legacy; do not add to them.
|
||||||
|
- Shared design-system widgets live in `packages/ui/` (`immich_ui`), with a preview per component in `lib/src/previews/`.
|
||||||
|
|
||||||
|
## Skills
|
||||||
|
|
||||||
|
- **Run codegen**: `mise //mobile:codegen` runs the Dart OpenAPI client, build_runner (`*.g.dart`, `*.freezed.dart`, `router.gr.dart`), Drift migrations and schema, ffigen, pigeon and translations. Run it after changing Drift tables, routes, pigeon definitions or freezed classes; `mise //mobile:codegen:watch` keeps build_runner running. CI job "Unit Test Mobile" regenerates and fails if the code does not compile.
|
||||||
|
- **Change the local database**: edit tables in `lib/data/db/main/table/` and the `@DriftDatabase` in `lib/data/db/main/database.dart`, bump the schema version, then `mise //mobile:codegen:drift:migration` writes `database.steps.dart` and a snapshot in `drift_schemas/main/`. Add a step migration and a test in `test/drift/main/`.
|
||||||
|
- **Add a platform channel**: define the API in `pigeon/<name>.dart`, run `mise //mobile:codegen:pigeon` to generate `lib/platform/<name>.g.dart` plus the Swift and Kotlin interfaces, then implement them in `ios/` and `android/`.
|
||||||
|
- **Tests and lint**: `mise //mobile:test`; one file with `flutter test test/path/foo_test.dart`. Tests live under `test/{unit,medium,domain,infrastructure,drift,presentation}/`; `test/medium/` has `repository_context.dart` and `service_context.dart` for tests against a real Drift database. `mise //mobile:lint` runs the analyzer and DCM; `mise //mobile:lint-fix` fixes what it can.
|
||||||
|
- **Preview a UI component**: `cd packages/ui && flutter widget-preview start` shows components from `lib/src/previews/` with a light/dark toggle.
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
# native/AGENTS.md
|
||||||
|
|
||||||
|
Rust code compiled into the mobile app. `crates/codecs` holds the image decoders (one feature each), `crates/native_core` the shared logic, and `crates/native_core_ffi` the C ABI whose header cbindgen writes to `include/native_core.h`. `native_core/` is the Flutter package with the build hook that compiles the crates from source and the ffigen bindings.
|
||||||
|
|
||||||
|
## Rules
|
||||||
|
|
||||||
|
- Every `unsafe` block needs a `// SAFETY:` comment; clippy denies `undocumented_unsafe_blocks` and `mise //native:lint` runs with `-D warnings`.
|
||||||
|
- The release profile keeps unwinding because the FFI boundary relies on `catch_unwind`; do not set `panic = "abort"`.
|
||||||
|
- When a crate dependency changes, update `native_core/NOTICES` with its license.
|
||||||
|
|
||||||
|
## Skills
|
||||||
|
|
||||||
|
- **Build, test and lint**: `mise //native:build`, `mise //native:test`, `mise //native:lint`, `mise //native:fmt`. The toolchain is pinned in `crates/native_core_ffi/rust-toolchain.toml` and `mise.toml`; keep them in sync.
|
||||||
|
- **Change the FFI surface**: edit `crates/native_core_ffi`, then run `mise //mobile:codegen:native` to rebuild the C header and regenerate the Dart bindings in `native_core/lib/src/bindings.g.dart`. The Flutter build recompiles the crates on the next `flutter run`.
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
# server/AGENTS.md
|
||||||
|
|
||||||
|
NestJS + Kysely + PostgreSQL, layered `controllers` → `services` → `repositories`. The process model and job system are described in `docs/docs/developer/architecture.mdx`.
|
||||||
|
|
||||||
|
## Rules
|
||||||
|
|
||||||
|
- ESM with no relative imports (lint-enforced). Import as `src/...js` and `test/...js`.
|
||||||
|
- Every route has `@Authenticated(...)` and `@Endpoint({ summary, description, history })`; the global `AuthGuard` throws otherwise. Controllers only call one service method with `(auth, dto)`.
|
||||||
|
- Services `extends BaseService` and declare no constructor; repositories are available as `this.xRepository`. Call `await this.requireAccess({ auth, permission, ids })` before touching a resource.
|
||||||
|
- Repositories are the only I/O layer and return rows. Services interpret results and throw `BadRequestException` etc.; repositories never throw HTTP exceptions.
|
||||||
|
- DTOs are Zod schemas via `nestjs-zod` (`createZodDto`, `.describe()`, `.meta({ id })`) in `src/dtos/`, with `mapX()` mappers beside them.
|
||||||
|
- Enums (`JobName`, `QueueName`, `Permission`, `BootstrapEventPriority`) live in `src/enum.ts`; job payload unions in `src/types.ts`. Admin-editable config and its `defaults` are in `src/dtos/config.dto.ts`; env vars in `src/dtos/env.dto.ts`.
|
||||||
|
- Codegen tasks (`migrations`, `sql`, `sync-open-api`) run from `dist/`, so `mise //server:build` first.
|
||||||
|
|
||||||
|
## Skills
|
||||||
|
|
||||||
|
- **Add an endpoint**: define the request/response DTOs in `src/dtos/` with a mapper, add the service method (call `requireAccess` first), add the controller method with `@Authenticated` and `@Endpoint` whose `history` uses `HistoryBuilder` (`.added('vN')`, `.beta`, `.stable`, `.deprecated`), then add a `*.controller.spec.ts` case and regenerate the API clients (root skill).
|
||||||
|
- **Add a repository**: update all of `BaseService` constructor and `BaseService.create()` in `src/services/base.service.ts`, `BASE_SERVICE_DEPENDENCIES`, `src/repositories/index.ts`, and `newTestService`/`getMocks` in `test/utils.ts`. Kysely repos inject `@InjectKysely() db: Kysely<DB>`.
|
||||||
|
- **Change the schema**: edit `src/schema/tables/*.table.ts` (`@immich/sql-tools` decorators plus `UpdatedAtTrigger`, `UpdateIdColumn`, `PrimaryGeneratedUuidV7Column` from `src/decorators.ts`). A new table is registered in both `ImmichDatabase` and the `DB` interface in `src/schema/index.ts`. Then, with Postgres reachable at `DB_URL` (default `postgres://postgres:postgres@localhost:5432/immich`): `mise //server:build`, `mise //server:migrations generate <Name>`, review the file, move it into `src/schema/migrations/`, and `mise //server:migrations sync-order`. `ORDER` is append-only. CI jobs "SQL Schema Checks" and "Verify migration ORDER" fail otherwise. `mise //server:migrations revert` moves the file out of `src/`; restore it and rebuild afterwards.
|
||||||
|
- **Regenerate SQL snapshots**: repository methods decorated with `@GenerateSql({ params: [DummyValue.UUID] })` have their SQL written to `src/queries/*.sql`. After changing one, run `mise //server:build` then `mise //:sql` with Postgres reachable. Use `@ChunkedArray`/`@ChunkedSet` on methods that take ID lists. CI job "SQL Schema Checks" fails if the snapshots are stale.
|
||||||
|
- **Add a background job**: add the `JobName` to `src/enum.ts` and its payload to the `JobItem` union in `src/types.ts`, then exactly one `@OnJob({ name, queue })` handler on a service (startup fails on zero or two). Chaining to follow-up jobs lives in `JobService.onDone`. In-process events use `@OnEvent` with `EventMap` in `src/repositories/event.repository.ts`.
|
||||||
|
- **Unit tests**: `src/**/*.spec.ts`. `const { sut, mocks } = newTestService(XService)` from `test/utils.ts` auto-mocks every repository; strict mocks fail when called without an implementation. Fixtures in `test/fixtures/*.stub.ts`, builders in `test/small.factory.ts` (`factory.auth()`, ...) and `test/factories/`. Run one file with `mise //server:test --run src/services/album.service.spec.ts`.
|
||||||
|
- **Controller tests**: `src/controllers/*.controller.spec.ts` use `controllerSetup(Controller, [{ provide: Service, useValue: mockBaseService(Service) }])`, which boots Nest with the real validation pipe and `AuthGuard`, and assert with supertest. They cover validation and auth wiring, not business logic.
|
||||||
|
- **Medium tests**: `test/medium/specs/{services,repositories,sync,exif,workflow}/`. They need Docker: `globalSetup` starts a Postgres testcontainer, migrates a template database, and each spec file gets a clone. Build `newMediumService(Service, { database, real: [AssetRepository, ...], mock: [JobRepository, ...] })` from `test/medium.factory.ts`; it returns `{ sut, ctx }` where `ctx.newUser()`, `ctx.newAsset()`, `ctx.get(Repo)` create data and reach repositories. Exif specs read from `e2e/test-assets`; workflow specs need `mise //:plugins` first. Run with `mise //server:test-medium --run test/medium/specs/services/album.service.spec.ts`.
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
# web/AGENTS.md
|
||||||
|
|
||||||
|
SvelteKit SPA (SSR off), Svelte 5 runes, Tailwind, `@immich/ui`, `@tanstack/svelte-query`. Talks to the server through the workspace `@immich/sdk`.
|
||||||
|
|
||||||
|
## Rules
|
||||||
|
|
||||||
|
- Import with the `#lib/*` alias and an explicit `.ts` extension: `import { authManager } from '#lib/managers/auth-manager.svelte.ts'`. Never `$lib`. Other aliases: `#i18n/*`, `#test-data/*`, `#tests/*`.
|
||||||
|
- Build URLs with the typed `Route` object in `src/lib/route.ts`. `continue`/`previousRoute` query params must go through `Route.continue(...)` (lint-enforced).
|
||||||
|
- Global state lives in `*.svelte.ts` singleton managers in `src/lib/managers/`. `eventManager` is the typed app-wide bus; subscribe with `eventManager.on({...})` or `components/OnEvents.svelte`. `src/lib/stores/` is legacy.
|
||||||
|
- `src/lib/services/*.service.ts` only return `@immich/ui` `ActionItem`s and open modals; data access belongs in `src/lib/queries/`.
|
||||||
|
- Unused variables must be named exactly `_`. `svelte-check` runs with `--fail-on-warnings`.
|
||||||
|
- `@immich/sdk` must be built before the web builds or tests: `mise //:sdk:install && mise //:sdk:build` (`mise //web:start` does this).
|
||||||
|
|
||||||
|
## Skills
|
||||||
|
|
||||||
|
- **Add a query or mutation**: one `src/lib/queries/<controller>.query.ts` per API controller. Export a query-key constant, `getXQuery(...)` factories returning `queryOptions({ queryKey, queryFn: ({ signal }) => sdkFn(..., { signal }), select })`, and `xMutation` options built with `asMutation(sdkFn, { meta: { success: { toast }, error: { toast } } })`. Cache updates and `eventManager` emits happen in the mutation's `onSuccess`. The global `MutationCache` in `src/lib/queries/client.ts` shows the toasts and calls `handleError`; `meta.error.toast: false` suppresses it. Outside components run mutations with `executeMutation(optionsFn, variables, mutateOptions)`.
|
||||||
|
- **Add a page**: put it under `src/routes/(user)/` for the authenticated app or `src/routes/admin/`. In `+page.ts`: `await authenticate(url)`, preload with `await queryClient.query(getXQuery())`, `const $t = await getFormatter()`, and return `{ meta: { title: $t('...') } }`. In the component read the data with `createPreloadedQuery(() => getXQuery())`. Route-specific components sit next to the route.
|
||||||
|
- **Add a modal**: create `src/lib/modals/XModal.svelte` with `Props = { onClose: (result?) => void }` built on `@immich/ui` `Modal`/`FormModal`. Open it with `const result = await modalManager.show(XModal, props)`; the promise resolves with whatever is passed to `onClose`.
|
||||||
|
- **Use a translation**: `import { t } from 'svelte-i18n'` and `$t('key')` in components; `const $t = await getFormatter()` from `#lib/utils/i18n.ts` elsewhere. Keys are typed against `i18n/en.json`, so add the key there first (root skill). Rich text goes through `src/lib/elements/FormatMessage.svelte`.
|
||||||
|
- **Unit tests**: vitest config is the `test` block in `vite.config.ts` (happy-dom). `src/test-data/setup.ts` initializes svelte-i18n so `$t` returns the key. Build data with the factories in `src/test-data/factories/`; shared mocks (SDK, observers) are in `src/lib/__mocks__/`. Run one file with `mise //web:test --run src/lib/utils/date-time.spec.ts`.
|
||||||
Reference in New Issue
Block a user