# Design ## Context См. `proposal.md` для мотивации. Технические факты, формирующие подход: - Генерация вариантов — `photos/pipeline.go:buildVariants`: `thumb = Fit(200,200)`, `preview = Fit(1200,1200)`, WebP q82. Оба кропа вписывают по длинной стороне. - Метаданные имеют две реализации (Postgres и SQLite) с общей контрактной проверкой `internal/repo/contract`; SQL дублируется (`photos/store.go`, `photos/sqlite_store.go`). - Роли заданы жёстко: `photo_objects.role CHECK (role IN ('original','thumb','preview'))` (`migrations/000008`, `migrations/sqlite/000001`). - Варианты генерирует фоновый воркер из очереди `variant_jobs`; лишние объекты удаляются сборщиком мусора по отсутствию ссылок (`photos/worker.go`). - Фронтенд-сетка берёт только `thumb_url` (`PhotoGrid.svelte:78`); обложки — только `cover_thumb_url` (`albums/store.go` джойнит `role='thumb'`). ## Goals / Non-Goals **Goals:** - Сетка фото и обзор альбомов показывают изображение без апскейла при любом выбранном размере. - Единое поведение на Postgres и SQLite; контрактные тесты покрывают новую роль. - Бэкфилл существующих фото без ручных операций. **Non-Goals:** - Динамоческое ресайзирование на лету / image-proxy. - Иные форматы (AVIF) или согласование форматов. - Изменение `preview` (просмотрщик) и оригинала. - Замер точной плотности DPR-пикселей и «идеальный» выбор варианта — допускается запас. ## Decisions - **Набор вариантов.** `thumb = Fill(200,200)` (квадрат), `medium = Fill(800,800)` (квадрат), `preview = Fit(1200,1200)`. Причина: плитка сетки квадратная (`aspect-square object-cover`), квадратный кроп исключает лишний апскейл короткой стороны; просмотрщик не квадратный, поэтому `preview` остаётся вписанным. Альтернатива — оставить `thumb` вписанным: отклонена, короткая сторона выходит < 200 и мылит уже мелкие плитки. - **Роль `medium` в `photo_objects`.** Роли маппятся 1:1 на поля API и на привязки; расширяем ограничение значением `medium`. Альтернатива — отдельная таблица вариантов: избыточна для одного размера. - **`srcset` + `sizes`.** Ширины `200w/800w/1200w`; `sizes` берётся из фактической ширины сетки (`bind:clientWidth` на `