# Отчёт: изменения, сделанные без спецификаций - **Дата:** 2026-10-21 - **Диапазон:** `45a953f^..eb2d478` — 21 коммит, 2026-10-20 — 2026-10-21 - **Ветка:** `develop` - **Предмет:** правки веб-интерфейса просмотра и нового инструмента «Свет» (редактирование фото), сделанные без обновления спецификаций OpenSpec - **Результат:** спеки дописаны (`748143c`), заведено изменение `fix-copy-edit-variant-sharing` ## 1. Метод 1. Разобран диапазон коммитов `45a953f^..eb2d478`: для каждого проверено, затронул ли он `openspec/specs`. 2. Для коммитов без спек — прочитаны затронутые участки кода (`frontend/src/lib/components/Viewer.svelte`, `PhotoEditPanel.svelte`, `PhotoPanel.svelte`, `frontend/src/lib/edit.ts`, `backend/internal/photos/worker.go`, `store.go`, `sqlite_store.go`). 3. Сопоставлено фактическое поведение с текстом существующих требований `ui-workspace`, `media-objects`, `photos`, `dev-make-workflow`. 4. Каждое найденное расхождение зафиксировано как требование (SHALL) со сценариями WHEN/THEN; после правок спеки проверены `openspec validate --specs`. ## 2. Сводка | Показатель | Значение | | --- | --- | | Коммитов в диапазоне | 21 | | Из них со спеками | 11 | | Из них без спек | 10 | | Затронутых спецификаций | 4 (`ui-workspace`, `media-objects`, `photos`, `dev-make-workflow`) | | Новых требований | 3 («График тональной кривой», «Навигация в зуме», «Сборка WASM-модуля фильтров») | | Уточнённых требований | 6 | | Новых сценариев | 12 | | Ошибок валидации | 0 (`23 passed, 0 failed`) | ## 3. Покрытие по коммитам | Коммит | Изменение | Где описано | | --- | --- | --- | | `45a953f` | скрытие боковых панелей, хоткеи размера миниатюр | `ui-workspace` → «Скрытие боковых панелей» (уже было) | | `50b9983` | размер миниатюр не меняется при скрытых панелях | там же, сценарий «Размер миниатюр при скрытых панелях» | | `35f8439` | зум клавишами `+` / `-` | `ui-workspace` → «Зум в просмотре» (уже было) | | `0a9e527` | лента миниатюр следует за текущим фото | уже было | | `961dff1` | `Ctrl+F` — полноэкранный режим приложения | уже было | | `afaaaab` | офлайн-режим с кешем эскизов и превью | `ui-workspace` → «Офлайн-режим веб-приложения» (уже было) | | `0548b1a` | вкладка «Редактирование», инструмент «Свет» | `ui-workspace` → «Вкладка «Редактирование»» (уточнено) | | `cd3d17c` | правки применяются сразу; вкладка не сбрасывается при обновлении фото | уточнено: «Обновление фото SHALL сохранять открытую вкладку панели» | | `6d3d7cb` | ускорение рендера правок в 9 раз | сознательно не специфицировано (внутренняя оптимизация) | | `4d2069d`, `520886a` | клиентский рендер правок, WASM-фильтры | «Фильтры считаются в браузере» (уже было) | | `2adcb13` | предпросмотр правок работает и без зума | уточнено: «Правки видны сразу — и во вписанном виде, и в зуме» | | `500ec10`, `f8ee937` | починка зума, центрирование | «Зум в просмотре»: `Space` = 100% **от оригинала по центру кадра** | | `b2a509f` | в зуме стрелки панорамируют фото | **новое** требование «Навигация в зуме» | | `2668d87` | искажение цветов в предпросмотре | уже было (сценарий про сверку с эталоном) | | `d904b2f` | невидимая линия тональной кривой | сознательно не специфицировано (багфикс) | | `b64574b` | гистограммы каналов в редакторе кривой | **новое** требование «График тональной кривой» | | `48ed0bb` | гистограмма пересчитывается при правке | там же, сценарий «Живая гистограмма» | | `332f35b`, `eb2d478` | сворачивание блока «Кривая», свёрнут по умолчанию | там же, сценарий «Сворачивание кривой» | | `520886a` | сборка WASM-модуля фильтров (`npm run wasm`, cargo) | `dev-make-workflow` → **новое** требование «Сборка WASM-модуля фильтров» | | `520886a`, `worker.go` | роли объектов `edit`, `edit_thumb`, `edit_medium`, `edit_preview` | `media-objects` → «Журнал объектов» + сценарий «Привязка отредактированной версии» | | `worker.go` | отдельная очередь рендера правок (`edit_jobs`) | `media-objects` → «Генерация вариантов» + «Рендер правок не блокирует варианты» | | `worker.go` | рендер правок по исходному объекту | `photos` → «Неразрушающее редактирование фотографии» + «Рендер по оригиналу» | ## 4. Новые и уточнённые требования **Новые требования:** - `ui-workspace` → «График тональной кривой»: гистограмма каналов, живой пересчёт, добавление и перетаскивание точек, сворачиваемый блок «Кривая» (по умолчанию свёрнут, кнопки выбора канала остаются доступными). - `ui-workspace` → «Навигация в зуме»: клавиши-стрелки панорамируют фото и не переключают соседние снимки, переключение остаётся за мышью и кнопками у краёв. - `dev-make-workflow` → «Сборка WASM-модуля фильтров»: Rust-исходники в `frontend/wasm/light`, обновление модуля перед сборкой статики, откат на готовый модуль из репозитория, если cargo недоступен. **Уточнённые требования:** - `ui-workspace` → «Вкладка «Редактирование»»: добавлено сохранение вкладки при обновлении фото; предпросмотр правок работает в любом режиме просмотра. - `ui-workspace` → «Зум в просмотре»: `Space` показывает 100% **от оригинала** по центру кадра. - `media-objects` → «Журнал объектов»: перечислены роли `edit`, `edit_thumb`, `edit_medium`, `edit_preview`. - `media-objects` → «Совместные объекты»: уточнено, что отредактированная версия создаётся и привязывается только к фото, чьи правки её породили. - `media-objects` → «Генерация вариантов»: рендер правок идёт в отдельной очереди. - `photos` → «Неразрушающее редактирование фотографии»: рендер идёт по исходному объекту, а не по уменьшенной копии. ## 5. Что сознательно осталось без спецификаций | Изменение | Почему | | --- | --- | | `6d3d7cb` — ускорение рендера в 9 раз | внутренняя оптимизация, внешнее поведение не изменилось | | `d904b2f` — видимость линии кривой | багфикс: требование «кривая показывается» уже было | | `500ec10` — контейнер прокрутки зума | деталь вёрстки, покрыта требованием «Панорамирование» | | правки переноса/центрирования внутри `f8ee937` | описаны пользовательские последствия, а не механизм | ## 6. Найденное расхождение кода и замысла `Store.Clone` и `SQLiteStore.Clone` копируют **все** привязки `photo_objects`, включая роли `edit*`, хотя воркер создаёт edit-объекты с владельцем `[]string{photoID}` и комментирует это так: «The edit belongs to this photo only, so the variants are not shared with the copies of the same original». Последствия: | Сценарий | Фактическое поведение | Ожидаемое | | --- | --- | --- | | Копия **до** правки | копия в исходном виде | так и должно быть | | Копия **после** рендера | копия показывает отредактированную картинку, но `edit_params` пусты: в панели правок параметров нет, первая же новая правка перезаписывает обработку с нуля | копия в исходном виде | Оформлено как изменение `fix-copy-edit-variant-sharing` (`openspec/changes/fix-copy-edit-variant-sharing/`): - `proposal.md` — зачем; - `design.md` — фильтр ролей в `Clone` вместо правки выдачи URL, единый список edit-ролей, разовая чистка существующих копий миграцией в обоих диалектах; - `specs/media-objects/spec.md` — дельта требования «Совместные объекты»; - `tasks.md` — 4 группы задач (список ролей → копирование → миграции → проверки). ## 7. Проверка ``` openspec validate --specs → 23 passed, 0 failed openspec validate fix-copy-edit-variant-sharing --type change → valid ``` ## 8. Реализация `fix-copy-edit-variant-sharing` Изменение реализовано полностью (9/9 задач): | Что | Где | | --- | --- | | Общий список edit-ролей `EditRoles()` и предикат `IsEditRole()` | `backend/internal/photos/edit_render.go` | | `Clone` не переносит edit-привязки (Postgres: `role <> ALL($3::text[])`) | `backend/internal/photos/store.go` | | То же для SQLite (`role NOT IN (...)`) | `backend/internal/photos/sqlite_store.go` | | Сброс правок использует общий список ролей | `backend/internal/photos/edit_service.go` | | Разовая чистка копий, созданных до фикса | `backend/migrations/000016_*.sql`, `backend/migrations/sqlite/000008_*.sql` | | Контрактная проверка копирования (оба хранилища) | `backend/internal/repo/contract/contract.go`, кейс `Clone` | | Сервисные тесты: копия и миграция | `backend/internal/photos/copy_test.go`, `backend/internal/e2e/copy_edit_test.go` | Проверки: - `make test` (с Postgres) — весь бэкенд зелёный; - контрактный кейс `Clone` проходит на SQLite и Postgres; без фикса он падает («clone shares the edited version»); - e2e на SQLite: копия показывает исходник, `edit_params` пусты, миграция убирает «чужие» привязки, не трогая привязки владельца; - ручная проверка через HTTP и браузер: в просмотре копии грузится `thumb.webp` (не `edit_preview`), все ползунки панели «Редактирование» в нуле. ## 9. Что дальше - Ревью и архивация изменения `/opsx-archive` — дельта `media-objects` переедет в основную спеку. - Изменение требует Postgres для полного прогона: в этом окружении поднимался временный контейнер `postgres:16-alpine` на порту 55432 (`TEST_DATABASE_URL='postgres://postgres:postgres@127.0.0.1:55432/galo?sslmode=disable'`).