# Tasks ## 1. Роль локального файла вместо состояния индекса - [x] 1.1 Добавить в `LocalAsset` (`data/sync/SyncPlan.kt`) признаки метки превью (`stampedOriginalHash`, `stampedAssetKey`), а в `DeviceLocalAssetProvider` читать `ProvenanceStamp` в момент пересчёта хеша файла; решение «локальный файл — созданное приложением превью» вынести в чистую функцию с юнит-тестом (`cd android && ./gradlew :app:testDebugUnitTest --tests "*ProvenanceStampTest" --tests "*SyncPlannerTest"`). - [x] 1.2 В `SyncPlanner.plan` не загружать превью и не удалять из-за него серверную копию: использовать метку как признак роли и идентичность оригинала, отдавать новое действие `SyncAction.MarkPreview`; покрыть `SyncPlannerTest` случаями «состояние индекса `LOCAL`, файл — превью с меткой» (ожидается `MarkPreview`), «другое содержимое без метки» (ожидается загрузка) и «превью потерявшего копию элемента» (`cd android && ./gradlew :app:testDebugUnitTest --tests "*SyncPlannerTest"`). - [x] 1.3 В `SyncCoordinator` применять `MarkPreview` (состояние `PREVIEW`) и не разгружать элементы с незавершённой загрузкой (`SyncUploadQueue.isPending`) в `offloadUploaded`; покрыть `SyncCoordinatorTest` случаями «расхождение состояния выправляется» и «загрузка в очереди — разгрузки нет» (`cd android && ./gradlew :app:testDebugUnitTest --tests "*SyncCoordinatorTest"`). - [x] 1.4 Распознавать постер разгруженного видео как превью исходного элемента по метке (`asset` указывает на видео, а не на постер) и не удалять серверную копию видео, уступившего место постеру; покрыть `SyncPlannerTest` случаями «постер связывается с видео, не загружается и не удаляется» и «видео без постера удаляется как раньше» (`cd android && ./gradlew :app:testDebugUnitTest --tests "*SyncPlannerTest"`). ## 2. Замена версии с подтверждением - [x] 2.1 Добавить `replacesPhotoId` в `UploadJobEntity`, миграцию `MIGRATION_8_9` (версия 9) и ветку в `MigrationTest`; проверить `cd android && ./gradlew :app:testDebugUnitTest --tests "*MigrationTest" --tests "*UploadRepositoryTest"`. - [x] 2.2 В `SyncPlanner` заменить пару «`Upload` + `DeleteOnServer`» для изменённого содержимого на `SyncAction.ReplaceUpload(asset, stalePhotoId)`, а в `SyncCoordinator` писать надгробие для прежней копии и ставить задание с полем замены; покрыть `SyncPlannerTest` (ожидается `ReplaceUpload`) и `SyncCoordinatorTest` (надгробие и задание с заменой) (`cd android && ./gradlew :app:testDebugUnitTest --tests "*SyncPlannerTest" --tests "*SyncCoordinatorTest"`). - [x] 2.3 Вынести сверку принятой контрольной суммы в чистую функцию и подключить её в `UploadWorker`: совпадение `original_checksum` с хешем задания удаляет прежнюю копию и записывает провенанс, расхождение или ошибка — не удаляет, не разгружает и оставляет задание с ошибкой; покрыть `SyncUploadTest` (`cd android && ./gradlew :app:testDebugUnitTest --tests "*SyncUploadTest"`). - [x] 2.4 Сервер: заполнять `p.OriginalChecksum` в `Service.Upload` до ответа и публикации события (существующее требование `photos` «Контрольная сумма оригинала» распространяется на данные созданного фото, но ответ на загрузку её не отдавал — живая проверка показала, что из-за этого отклонялись все синхронные загрузки); покрыть тестом ответа на загрузку (`cd backend && go test ./internal/app/ -run TestUploadResponseExposesOriginalChecksum -v`). - [x] 2.5 Клиент остаётся устойчивым к инстансу без контрольной суммы: «не сообщено» SHALL NOT считаться расхождением — загрузка принимается, а прежняя копия не удаляется; покрыть `SyncUploadTest` случаями «не сообщено» и «нет заявки на содержимое» (`cd android && ./gradlew :app:testDebugUnitTest --tests "*SyncUploadTest"`). ## 3. Состояние после возврата оригинала - [x] 3.1 В `RestoreManager` помечать восстановленный элемент `UPLOADED` (оригинал и серверная копия) вместо `LOCAL`; покрыть `RestoreManagerTest` и `GallerySyncPolicyTest` случаем «после возврата повторной разгрузки нет» (`cd android && ./gradlew :app:testDebugUnitTest --tests "*RestoreManagerTest" --tests "*GallerySyncPolicyTest"`). ## 5. Интеграционная проверка - [x] 5.1 Прогнать весь Android-набор и собрать debug-APK (`cd android && ./gradlew test :app:assembleDebug`). ## Workflow follow-up - Восстановление пострадавших оригиналов (бывшая задача 4) — ручная процедура владельца инстанса, выполняется вне change, креды в репозиторий и артефакты не попадают: - вернуть из корзины `6d4e1897-3a7e-4cba-9d85-5bbb4b06f5a4` (IMG_20261004_163151.jpg, 3 437 694 Б, 3072x4096) и `991ea4ee-debb-4ecb-bc26-e4d1a2140085` (IMG_20261009_153136.jpg, 2 226 043 Б, 3072x4096) в альбом `N2` (`POST /api/photos/{id}/restore`); - удалить деградировавшие копии `b0b29905-712b-4143-9d8c-0b74aa567ce5` и `1f49e8fb-ea9c-4764-8daa-7b185be107e9` (`DELETE /api/photos/{id}`); - проверить, что в альбоме осталось по одной записи с каждым именем и оригинал у обеих 3072x4096. - Уборка после регрессии первой версии сверки контрольной суммы (живая проверка 2026-10-16): в новом альбоме `U2` (`e7ae31c9-805c-42f5-8fd2-ea2304f0c485`) лежат 19 мягко удалённых копий десяти файлов, которые приложение само же и отклонило. Их список (id и имена) — в отчёте; удаляются точечно `DELETE /api/photos/{id}/permanent`. Корзину целиком очищать нельзя, пока не восстановлены оригиналы `N2`: они лежат там же. - Заархивировать изменение после прохождения ревью проекта. - Проверить результат архивации.