Распознаём ручное удаление источника (раздача в qBittorrent) и/или цели (разложенные хардлинки) и отражаем его в состоянии задачи, без автодействий. - Новая capability state-reconciliation (OpenSpec): фоновая сверка по матрице «источник × цель» → состояния target_missing/orphaned/deleted, переходы и самовосстановление (healing). - worker: reconcileDesync в Poll (только разложенные/desync-задачи), дебаунс пропажи источника (порог [worker].source_missing_threshold) и синхронный preflight перед действиями (relink/recognize/apply/undo) — не доверяем state в БД. - layout.Undo: отказ снять последнюю копию (nlink<=1 или нет источника), отказ всего батча без частичного отката (ErrLastCopy). - store: единый список terminalStates для IsTerminal и FindActiveByInfohash (иначе семантика «активности» разъезжается), столбец source_miss_count, миграция 0003. - httpapi/web и Telegram: показ новых состояний и уведомления о рассинхроне. - Доки: workflow.md, jellyfin-layout.md, database.md (+0003), config. Change заархивирован в openspec/changes/archive, дельта влита в openspec/specs. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
96 lines
6.6 KiB
Markdown
96 lines
6.6 KiB
Markdown
## 1. Хранилище и состояния
|
||
|
||
- [x] 1.1 Добавить значения состояний `target_missing`, `orphaned`, `deleted`
|
||
в `internal/store` (константы `State*`) и в перечень допустимых состояний.
|
||
- [x] 1.2 Миграция goose `0003_*`: `ALTER TABLE download ADD COLUMN
|
||
source_miss_count INTEGER NOT NULL DEFAULT 0`; обновить модель `Download`.
|
||
- [x] 1.3 Запросы в `store`: выборка desync-кандидатов
|
||
(`done`/`target_missing`/`orphaned`/`deleted`), чтение/сброс/инкремент
|
||
`source_miss_count`, чтение `file_link` (`status = linked`) по задаче.
|
||
|
||
## 2. Конфигурация
|
||
|
||
- [x] 2.1 Добавить `[worker].source_missing_threshold` (int, дефолт `3`) в
|
||
`internal/config` с валидацией (`>= 1`); пробросить в `worker.Config`.
|
||
- [x] 2.2 Отразить ключ в примере конфига и `docs/conventions/config.md`/
|
||
README, если там перечислены ключи `[worker]`.
|
||
|
||
## 3. Сверка с реальностью (worker)
|
||
|
||
- [x] 3.1 Выделить общий помощник `probe(download) → (sourcePresent,
|
||
targetPresent)` (источник: infohash в qBit; цель: `Lstat` всех
|
||
`file_link.dst_path` со `status = linked`, частичная пропажа = цель
|
||
отсутствует) и `deriveState(src, tgt) → State` — единая точка правды для
|
||
фона и preflight.
|
||
- [x] 3.2 В `Poll` для desync-кандидатов вызвать `probe` и реализовать
|
||
дебаунс источника: инкремент `source_miss_count` при отсутствии, сброс при
|
||
обнаружении; «источник удалён» только при `source_miss_count >= threshold`.
|
||
- [x] 3.3 Применить `deriveState` и переходить только при изменении:
|
||
`done`/`target_missing`/`orphaned`/`deleted` + healing обратно в `done`.
|
||
Переиспользовать `transition`.
|
||
- [x] 3.4 Не трогать фоновой сверкой активные и пользовательски-терминальные
|
||
состояния (`reverted`/`cancelled`/`failed`/`stuck` и активные).
|
||
|
||
## 3a. Синхронный preflight перед действием
|
||
|
||
- [x] 3a.1 Перед командами, требующими источника/цели (relink, «Распознать
|
||
заново», «Уточнить», `Apply`, `Undo`), вызывать `probe` **без дебаунса**
|
||
(единичная немедленная проба), не доверяя `state` в БД.
|
||
- [x] 3a.2 При неуспехе предусловия: действие не выполнять, прогнать
|
||
`deriveState` (привести `state` к реальности, напр. `done → orphaned`),
|
||
вернуть пользователю причину; при недоступности qBittorrent — отказ
|
||
«источник недоступен».
|
||
|
||
## 4. Безопасный Undo (layout + worker)
|
||
|
||
- [x] 4.1 `internal/layout` `Undo`: для каждой ссылки `Lstat(dst)` (нет —
|
||
пропустить идемпотентно), иначе проверить `nlink <= 1` и наличие
|
||
`src_path`; при «последней копии» — отказ с типизированной ошибкой, без
|
||
`unlink`.
|
||
- [x] 4.2 `worker.Undo`: отклонять команду для задачи в `orphaned` сразу с
|
||
понятным сообщением; при отказе `layout.Undo` — не переводить в `reverted`,
|
||
пробросить причину пользователю.
|
||
- [x] 4.3 Разрешить переход `target_missing → recognizing` в команде
|
||
«Привязать заново» (наряду с `reverted`/`cancelled`).
|
||
|
||
## 5. Уведомления
|
||
|
||
- [x] 5.1 Добавить события `EventOrphaned`/`EventTargetMissing` и слать
|
||
уведомление автору в `transition` при входе в эти состояния (как для
|
||
`review`/`done`), неблокирующе и вне `w.mu`.
|
||
|
||
## 6. Транспорты (httpapi + web-UI)
|
||
|
||
- [x] 6.1 Отобразить новые состояния в списке/карточке загрузки (метки,
|
||
пояснения «разложено, но файлов нет» / «источник удалён — последняя копия»).
|
||
- [x] 6.2 Скрыть/заблокировать `Undo` для `orphaned`; показать команду
|
||
«Привязать заново» для `target_missing`.
|
||
|
||
## 7. Тесты
|
||
|
||
- [x] 7.1 Таблица переходов сверки: все четыре ячейки матрицы + healing,
|
||
частичная пропажа цели → `target_missing`.
|
||
- [x] 7.2 Дебаунс: пропажа < порога не помечает; >= порога помечает; возврат
|
||
сбрасывает счётчик.
|
||
- [x] 7.3 `layout.Undo`: отказ при `nlink <= 1`/отсутствии `src_path`;
|
||
снятие лишнего хардлинка при живом источнике; пропуск отсутствующей цели.
|
||
- [x] 7.4 `worker.Undo` отклоняется для `orphaned`; relink из
|
||
`target_missing` ведёт в `recognizing`.
|
||
- [x] 7.5 Preflight: команда с устаревшим `state = done`, но удалённым
|
||
источником немедленно отказывает и приводит состояние к `orphaned`/
|
||
`deleted` (не дожидаясь фоновой сверки).
|
||
|
||
## 8. Документация и спеки
|
||
|
||
- [x] 8.1 Обновить `docs/specs/workflow.md`: граф состояний (+`target_missing`/
|
||
`orphaned`/`deleted`, переходы, healing) и описания.
|
||
- [x] 8.2 Обновить `docs/specs/jellyfin-layout.md`: инвариант безопасного
|
||
`Undo` (не снимать последнюю копию).
|
||
- [x] 8.2a Обновить ER-схему `docs/specs/database.md`: столбец
|
||
`download.source_miss_count` + отметка миграции `0003` (конвенция:
|
||
схема едет вместе с миграцией).
|
||
- [x] 8.3 Снять пункт «Рассинхрон состояния с реальностью» (часть про
|
||
detection/marking и undo-guard) из `docs/todo.md` или сузить до path 2.
|
||
- [x] 8.4 `openspec validate --strict`; ревью кода; затем `opsx:archive`
|
||
(влить дельту `state-reconciliation` в `openspec/specs/`).
|