chore(openspec): архивировать fix-copy-edit-variant-sharing и синхронизировать спеки
Дельта media-objects перенесена в основную спеку: копирование больше не переносит отредактированную версию, копия показывается в исходном виде и не наследует параметры правок. Change перемещён в архив за 2026-10-21.
This commit is contained in:
@@ -0,0 +1,120 @@
|
||||
# Design
|
||||
|
||||
## Context
|
||||
|
||||
См. `proposal.md` — Why. Ограничения, важные для подхода:
|
||||
|
||||
- `Store.Clone` (Postgres) и `SQLiteStore.Clone` (SQLite) копируют привязки
|
||||
`photo_objects` одной вставкой `INSERT ... SELECT` без фильтра по ролям, поэтому
|
||||
копия наследует и обычные варианты, и edit-варианты.
|
||||
- Обычные варианты действительно должны разделяться: воркер создаёт их через
|
||||
`createObject(ctx, siblings, ...)` , где `siblings` — все фото, делящие оригинал
|
||||
(`PhotosByOriginalObject`). Edit-варианты, наоборот, создаются с владельцем
|
||||
`[]string{photoID}` и привязаны только к одному фото.
|
||||
- Параметры правок лежат в колонке `photos.edit_params` и при клонировании не
|
||||
переносятся: `INSERT INTO photos (...)` в `Clone` её не включает. То есть рассинхрон
|
||||
возникает именно на уровне привязок объектов.
|
||||
- `ResetEdit` удаляет и параметры, и привязки edit-объектов
|
||||
(`DeleteObjectBindings`). Значит, у фото с пустыми `edit_params` edit-привязок быть
|
||||
не должно — кроме как последствия копирования.
|
||||
- Выдача URL смотрит только на привязки (`attachURLs`), а фронтенд выбирает
|
||||
`edit_thumb_url ?? thumb_url`. Привязка — источник правды о том, какие варианты
|
||||
есть у фото; чинить выдачу URL вместо привязки нельзя.
|
||||
- `ClaimGarbage` собирает готовые объекты без привязок, поэтому удаление привязки
|
||||
само по себе освобождает объект — отдельная уборка не нужна.
|
||||
|
||||
## Goals / Non-Goals
|
||||
|
||||
**Goals:**
|
||||
|
||||
- Копия фото не наследует отредактированную версию: показывается исходник, параметры
|
||||
правок пусты.
|
||||
- Postgres и SQLite ведут себя одинаково, список исключаемых ролей задан один раз.
|
||||
- Уже существующие копии в базе перестают показывать отредактированную версию без
|
||||
ручного вмешательства.
|
||||
|
||||
**Non-Goals:**
|
||||
|
||||
- Не переносим параметры правок на копию: правка принадлежит фото, а не копии.
|
||||
- Не рендерим отредактированную версию заново для копии.
|
||||
- Не меняем поведение переноса (`MoveBatch`) и разделяемых обычных вариантов.
|
||||
- Не трогаем клиентов и HTTP API: формат ответа не меняется.
|
||||
|
||||
## Decisions
|
||||
|
||||
### 1. Фильтровать роли в `Clone`, а не в выдаче URL
|
||||
|
||||
Копирование исключает edit-роли из переносимых привязок:
|
||||
|
||||
```
|
||||
INSERT INTO photo_objects (photo_id, object_id, role)
|
||||
SELECT $1, object_id, role FROM photo_objects
|
||||
WHERE photo_id = $2 AND role <> ALL($3)
|
||||
```
|
||||
|
||||
- **Альтернатива: прятать edit-URL, если у фото нет `edit_params`.** Отклонено:
|
||||
привязка осталась бы в базе, GC считал бы объект живым, а модель и привязка
|
||||
расходились бы. Привязка — источник правды.
|
||||
- **Альтернатива: копировать и параметры, и готовую версию.** Отклонено: меняет
|
||||
семантику «правка принадлежит фото» и требует отдельного рендера для копии.
|
||||
|
||||
### 2. Список edit-ролей — в одном месте
|
||||
|
||||
Роли уже объявлены в `backend/internal/photos/edit_render.go`
|
||||
(`RoleEdit`, `RoleEditThumb`, `RoleEditMedium`, `RoleEditPreview`), оба хранилища
|
||||
находятся в том же пакете. Добавляется общий список ролей и хелпер проверки, чтобы
|
||||
Postgres и SQLite не расходились и в SQL не попадали строковые литералы.
|
||||
|
||||
- **Альтернатива: писать роли литералами в каждом `Clone`.** Отклонено: список
|
||||
размножится, добавление новой edit-роли забудет одно из хранилищ.
|
||||
|
||||
### 3. Разовая чистка существующих копий — миграцией
|
||||
|
||||
В базе могут остаться привязки `edit*` у фото с пустыми `edit_params` — это и есть
|
||||
следы бага копирования. Чистка выполняется миграцией в обоих диалектах
|
||||
(Postgres и SQLite нумеруются независимо):
|
||||
|
||||
```
|
||||
DELETE FROM photo_objects
|
||||
WHERE role IN ('edit', 'edit_thumb', 'edit_medium', 'edit_preview')
|
||||
AND photo_id IN (SELECT id FROM photos WHERE edit_params IS NULL OR edit_params = '{}')
|
||||
```
|
||||
|
||||
- **Альтернатива: чистить лениво при чтении.** Отклонено: правило прячется в двух
|
||||
местах и не отражается в данных.
|
||||
- **Альтернатива: ничего не чистить.** Отклонено: старые копии продолжат показывать
|
||||
отредактированную версию, и расхождение останется в данных.
|
||||
- Удалённые привязки не осиротевают объекты: отредактированная версия остаётся
|
||||
привязана к своему фото, поэтому её объекты живут, пока живёт фото, и обычный
|
||||
`ClaimGarbage` уберёт их вместе с ним.
|
||||
|
||||
### 4. Проверка — в существующем тесте копирования
|
||||
|
||||
`backend/internal/photos/copy_test.go` уже проверяет, что копия делит объекты с
|
||||
оригиналом. Добавляется сценарий: после рендера правок копия получает обычные
|
||||
варианты, но не edit-объекты, и её `edit_params` пусты.
|
||||
|
||||
## Risks / Trade-offs
|
||||
|
||||
- [Копии, созданные до фикса, продолжат показывать правки до применения миграции] →
|
||||
Миграция чистит привязки у фото с пустыми `edit_params`; после неё поведение
|
||||
единообразно.
|
||||
- [Пользователь может потерять «неожиданно» отредактированную копию] → Копия без
|
||||
параметров правок всё равно не позволяла их править осмысленно: первая новая правка
|
||||
перезаписывала обработку с нуля.
|
||||
- [Опечатка в списке ролей оставит edit-привязку в копии] → Список ролей берётся из
|
||||
общих констант, а тест проверяет конкретные роли.
|
||||
- [Миграция удалит привязки у фото, которые потеряли `edit_params` не из-за копирования]
|
||||
→ Таких фото нет: `ResetEdit` удаляет и параметры, и привязки одновременно.
|
||||
|
||||
## Migration Plan
|
||||
|
||||
1. Миграция Postgres (`000016_*`) и SQLite (`000008_*`) удаляют «чужие» edit-привязки.
|
||||
Обе идемпотентны (`DELETE` без `IF EXISTS` по данным).
|
||||
2. Изменения в `Clone` — только код, схема не меняется.
|
||||
3. Откат: миграции `down` возвращают удалённые привязки не могут — откат выполняется
|
||||
только откатом кода; правки фото при этом сохраняются у исходных фото.
|
||||
|
||||
## Open Questions
|
||||
|
||||
Нет.
|
||||
Reference in New Issue
Block a user