Пересканирование Jellyfin: расширить триггер на reverted и deleted

Скан Jellyfin (POST /Library/Refresh) слался только при входе в done.
После Undo (reverted) и Delete (deleted) наши хардлинки сняты, а Jellyfin
держал битые записи до скана по расписанию.

Гейт скана в едином чекпоинте transitionErr переведён с state == done на
предикат triggersScan(state) по множеству {done, reverted, deleted}: гейт по
состоянию-цели естественно ловит пользовательские Undo/Delete и
reconcile-производный deleted, идемпотентно. target_missing/orphaned —
промежуточный рассинхрон (ждём relink/лечения) — исключены.

OpenSpec: заведена и влита дельта file-layout (требование
«Пересканирование Jellyfin после изменения библиотечных ссылок»); change
архивирован. Синк рукописных доков architecture.md/workflow.md. Тесты:
скан стреляет на reverted и deleted, молчит на входе вне множества.
Закрыта задача беклога jellyfin-skan-posle-udaleniya.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
av
2026-07-17 21:14:18 +03:00
co-authored by Claude Opus 4.8
parent 0354a8c96b
commit 1639ebfdd7
10 changed files with 303 additions and 72 deletions
@@ -0,0 +1,57 @@
## Why
Пересканирование Jellyfin сейчас дёргается **только** при входе в `done` (после
успешной раскладки). Но наши библиотечные хардлинки меняются ещё в двух случаях:
**Undo** (`done → reverted`) и **Delete** (`… → deleted`) снимают ссылки. После них
Jellyfin продолжает показывать записи с битыми путями до следующего скана по
расписанию — рассинхрон видимого каталога с реальностью, который мы уже умеем
чинить, но не сигналим.
Интеграция готова целиком (`internal/jellyfin`, конфиг `[jellyfin]`, проводка
`SetScanner`) — не хватает лишь расширить условие срабатывания. Точка правки —
единый чекпоинт `transitionErr` (`internal/worker/worker.go`), через который уже
проходят оба пути снятия ссылок (`Undo`, `Delete`) и reconcile-производный
`deleted`.
## What Changes
- Расширить гейт пересканирования Jellyfin в `transitionErr` с
`state == done` на множество состояний **входа**, где наши библиотечные
хардлинки только что изменились: `done` (файлы разложены), `reverted` (Undo снял
ссылки), `deleted` (Delete снял ссылки / сверка констатировала их отсутствие).
- Гейт **по состоянию-цели** в едином чекпоинте: он естественно ловит и
пользовательские Undo/Delete, и reconcile-производный `deleted` — это
задумано и идемпотентно (лишний скан безвреден, инкрементальный скан дёшев).
- `target_missing``orphaned`) в множество **не** включаем: это промежуточные
состояния рассинхрона, где раскладка ещё не «улеглась» — источник жив, задача
ждёт relink/восстановления и может залечиться обратно в `done`. Скан там
откладываем, чтобы не слать его на каждое колебание сверки; когда задача
придёт в `done`/`deleted`, скан сработает по общему правилу.
- Зафиксировать поведение в спеке `file-layout` (сейчас про Jellyfin-скан в
`openspec/specs/` нет ни слова) и поправить рукописные доки, где формулировка
«при входе в `done`» стала неверной.
## Capabilities
### New Capabilities
<!-- нет новых capability -->
### Modified Capabilities
- `file-layout`: фиксируется триггер пересканирования Jellyfin — не только после
раскладки (`done`), но и после снятия наших библиотечных хардлинков
(`reverted`, `deleted`), неблокирующе и опционально (`[jellyfin]`).
## Impact
- Код воркера: `internal/worker/worker.go` — расширить условие скана в
`transitionErr` (`state == done` → множество `{done, reverted, deleted}`),
обновить поясняющий комментарий.
- Тесты: `internal/worker/review_test.go` — позитивные тесты, что скан стреляет
на `reverted` (после `Undo`) и `deleted` (после `Delete`); при желании
негативный (скан не стреляет на входе, не меняющем наши ссылки).
- Доки: `docs/specs/architecture.md` («Пересканирование Jellyfin»),
`docs/specs/workflow.md` — формулировку «при входе в `done`» заменить на
«после раскладки и после снятия наших ссылок (Undo/Delete)».
- Данные/инварианты: не затрагиваются. Скан по-прежнему неблокирующий, вне
`w.mu`, в фоновом ctx; недоступность Jellyfin на состояние задачи не влияет.
Источник неприкосновенен — скан лишь читает библиотеку.
@@ -0,0 +1,56 @@
## ADDED Requirements
### Requirement: Пересканирование Jellyfin после изменения библиотечных ссылок
При сконфигурированном пересканировании Jellyfin (секция `[jellyfin]` включена) система SHALL при входе задачи в одно из состояний множества `{done, reverted, deleted}` **неблокирующе** просить Jellyfin пересканировать медиатеку (`POST /Library/Refresh`, скан всех библиотек). Эти три состояния — точки, где раскладка задачи **улеглась** так, что видимый Jellyfin каталог мог рассинхронизироваться с диском: `done` — наши хардлинки разложены (или восстановлены сверкой); `reverted` — Undo снял наши ссылки; `deleted` — ссылки сняты (Delete) либо констатировано их отсутствие (сверка), задача терминальна.
Условие срабатывания система SHALL проверять **по состоянию-цели перехода** в
едином чекпоинте записи состояния. Такой гейт SHALL естественно покрывать как
пользовательские команды (Undo → `reverted`, Delete → `deleted`), так и
reconcile-производный `deleted` — инициатор перехода роли не играет; повторный/
лишний скан безвреден (инкрементальный скан дёшев, операция идемпотентна).
Состояния **вне** этого множества система сканировать SHALL NOT. Сюда входят как
входы, не меняющие наши ссылки (`review`, `linking`, `cancelled` через Dismiss),
так и **промежуточные состояния рассинхрона** `target_missing` и `orphaned`: там
раскладка ещё не улеглась — задача ждёт relink/восстановления и может
«залечиться» обратно в `done`, поэтому скан на них система откладывает, а не шлёт
на каждое колебание сверки. `target_missing` система не сканирует сознательно,
хотя цель там пропала: это внешняя пропажа при живом источнике, не наше снятие.
Скан система SHALL выполнять **вне** блокировки воркера, в фоновом контексте и в
отдельной горутине, со scoped-логгером задачи для корреляции. Недоступность
Jellyfin на состояние задачи влиять SHALL NOT — ошибка вызова лишь логируется
(её пишет клиент Jellyfin как запись внешнего вызова). Если пересканирование не
сконфигурировано (`[jellyfin]` выключено), скан не дёргается ни в одном из этих
переходов.
#### Scenario: Скан после раскладки
- **GIVEN** пересканирование Jellyfin включено
- **WHEN** задача входит в `done` после успешной раскладки хардлинков
- **THEN** система неблокирующе дёргает `POST /Library/Refresh`
#### Scenario: Скан после отката (Undo)
- **GIVEN** пересканирование Jellyfin включено, задача в `done` с разложенными ссылками
- **WHEN** пользователь выполняет Undo и задача входит в `reverted` (наши ссылки сняты)
- **THEN** система неблокирующе дёргает `POST /Library/Refresh`
#### Scenario: Скан после удаления (Delete)
- **GIVEN** пересканирование Jellyfin включено, задача в `done`
- **WHEN** пользователь выполняет Delete и задача входит в `deleted` (наши ссылки сняты)
- **THEN** система неблокирующе дёргает `POST /Library/Refresh`
#### Scenario: Без конфигурации Jellyfin скан не дёргается
- **GIVEN** пересканирование Jellyfin выключено (`[jellyfin]` не сконфигурировано)
- **WHEN** задача входит в `done`, `reverted` или `deleted`
- **THEN** система скан не дёргает
#### Scenario: Вход вне множества не сканирует
- **GIVEN** пересканирование Jellyfin включено
- **WHEN** задача входит в состояние вне `{done, reverted, deleted}` (например, `review` или промежуточный `target_missing`)
- **THEN** система скан не дёргает
@@ -0,0 +1,34 @@
## 1. worker — расширить гейт скана
- [x] 1.1 В `internal/worker/worker.go` (`transitionErr`) заменить условие
`state == store.StateDone` на проверку принадлежности `state` множеству
`{done, reverted, deleted}` (небольшой предикат-хелпер для читаемости).
Остальную механику скана (фоновый ctx, `capFileLayout`-scoped-логгер,
неблокирующая горутина, `w.scanner != nil`) переиспользовать как есть.
- [x] 1.2 Обновить поясняющий комментарий: скан не только «раскладка завершена»,
а «наши библиотечные хардлинки изменились» (разложены при `done`, сняты при
`reverted`/`deleted`).
## 2. Тесты
- [x] 2.1 `internal/worker/review_test.go`: тест, что скан стреляет на входе в
`reverted` (после `Undo`).
- [x] 2.2 Тест, что скан стреляет на входе в `deleted` (после `Delete`).
- [x] 2.3 (Опц.) Негативный точечный тест: на входе, не меняющем наши ссылки,
скан не дёргается.
## 3. Доки
- [x] 3.1 `docs/specs/architecture.md` («Пересканирование Jellyfin» и строка
«Решённые вопросы»): формулировку «при входе в `done`» заменить на «после
раскладки (`done`) и после снятия наших ссылок (`reverted`/`deleted`)».
- [x] 3.2 `docs/specs/workflow.md`: в описании `done` и снятия ссылок отразить,
что скан дёргается и после Undo/Delete.
## 4. Ревью и сверка
- [x] 4.1 `task test` и `task lint` зелёные.
- [x] 4.2 Ревью кода (второй чекпоинт) перед archive.
- [x] 4.3 `openspec validate 2026-07-17-jellyfin-scan-on-revert-delete --strict` зелёный.
- [x] 4.4 Синк дельты в `openspec/specs/file-layout`, архив change; удалить
`docs/backlog/jellyfin-skan-posle-udaleniya.md` и строку из индекса беклога.
+55
View File
@@ -228,3 +228,58 @@ recognition(is_current)` плюс проверка существования п
- **WHEN** строится план раскладки новой загрузки с этим матчем
- **THEN** раскладка не выполняется, задача переходит в `review` с причиной рассинхрона папок тайтла
### Requirement: Пересканирование Jellyfin после изменения библиотечных ссылок
При сконфигурированном пересканировании Jellyfin (секция `[jellyfin]` включена) система SHALL при входе задачи в одно из состояний множества `{done, reverted, deleted}` **неблокирующе** просить Jellyfin пересканировать медиатеку (`POST /Library/Refresh`, скан всех библиотек). Эти три состояния — точки, где раскладка задачи **улеглась** так, что видимый Jellyfin каталог мог рассинхронизироваться с диском: `done` — наши хардлинки разложены (или восстановлены сверкой); `reverted` — Undo снял наши ссылки; `deleted` — ссылки сняты (Delete) либо констатировано их отсутствие (сверка), задача терминальна.
Условие срабатывания система SHALL проверять **по состоянию-цели перехода** в
едином чекпоинте записи состояния. Такой гейт SHALL естественно покрывать как
пользовательские команды (Undo → `reverted`, Delete → `deleted`), так и
reconcile-производный `deleted` — инициатор перехода роли не играет; повторный/
лишний скан безвреден (инкрементальный скан дёшев, операция идемпотентна).
Состояния **вне** этого множества система сканировать SHALL NOT. Сюда входят как
входы, не меняющие наши ссылки (`review`, `linking`, `cancelled` через Dismiss),
так и **промежуточные состояния рассинхрона** `target_missing` и `orphaned`: там
раскладка ещё не улеглась — задача ждёт relink/восстановления и может
«залечиться» обратно в `done`, поэтому скан на них система откладывает, а не шлёт
на каждое колебание сверки. `target_missing` система не сканирует сознательно,
хотя цель там пропала: это внешняя пропажа при живом источнике, не наше снятие.
Скан система SHALL выполнять **вне** блокировки воркера, в фоновом контексте и в
отдельной горутине, со scoped-логгером задачи для корреляции. Недоступность
Jellyfin на состояние задачи влиять SHALL NOT — ошибка вызова лишь логируется
(её пишет клиент Jellyfin как запись внешнего вызова). Если пересканирование не
сконфигурировано (`[jellyfin]` выключено), скан не дёргается ни в одном из этих
переходов.
#### Scenario: Скан после раскладки
- **GIVEN** пересканирование Jellyfin включено
- **WHEN** задача входит в `done` после успешной раскладки хардлинков
- **THEN** система неблокирующе дёргает `POST /Library/Refresh`
#### Scenario: Скан после отката (Undo)
- **GIVEN** пересканирование Jellyfin включено, задача в `done` с разложенными ссылками
- **WHEN** пользователь выполняет Undo и задача входит в `reverted` (наши ссылки сняты)
- **THEN** система неблокирующе дёргает `POST /Library/Refresh`
#### Scenario: Скан после удаления (Delete)
- **GIVEN** пересканирование Jellyfin включено, задача в `done`
- **WHEN** пользователь выполняет Delete и задача входит в `deleted` (наши ссылки сняты)
- **THEN** система неблокирующе дёргает `POST /Library/Refresh`
#### Scenario: Без конфигурации Jellyfin скан не дёргается
- **GIVEN** пересканирование Jellyfin выключено (`[jellyfin]` не сконфигурировано)
- **WHEN** задача входит в `done`, `reverted` или `deleted`
- **THEN** система скан не дёргает
#### Scenario: Вход вне множества не сканирует
- **GIVEN** пересканирование Jellyfin включено
- **WHEN** задача входит в состояние вне `{done, reverted, deleted}` (например, `review` или промежуточный `target_missing`)
- **THEN** система скан не дёргает