Привёл набор capabilities в OpenSpec к цепочке обработки, чтобы имя capability отвечало одному поведению. Чисто по спекам, код и поведение системы не меняются. Change refactor-capability-boundaries (архивирован): - recognition разделён на recognition (разбор LLM) + metadata-match (сверка с базами) - review выделен из web-ui + мигрирован из docs/specs/review-ux.md - новые capability из docs/specs: file-layout, download-tracking, notifications - identity очищен до инфра-id; приём (инфохэши, дедуп, ядро приёма) — в ingest - уведомление о рассинхроне перенесено из state-reconciliation в notifications - дубль владения путём и безопасного undo оставлен в state-reconciliation Итог: 11 capabilities, openspec validate --strict проходит (+37/−11 требований). Источник истины по мигрированным темам переехал в openspec/specs (шапки в docs). Снят пункт беклога «Пересмотр набора capabilities». Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
203 lines
15 KiB
Markdown
203 lines
15 KiB
Markdown
## Context
|
||
|
||
Набор capabilities в `openspec/specs/` сложился по ходу пилотной миграции и не
|
||
отражает цепочку обработки загрузки. Три проблемы:
|
||
|
||
1. `recognition` смешивает разбор LLM и работу с метабазами.
|
||
2. Поведение ревью размазано: часть — в `web-ui`, основная часть — в не
|
||
перенесённом `docs/specs/review-ux.md`.
|
||
3. Звенья цепочки `file-layout`, `download-tracking`, `notifications` в OpenSpec
|
||
отсутствуют — источник истины по ним в `docs/specs/` (`jellyfin-layout.md`,
|
||
`workflow.md`, `architecture.md`).
|
||
|
||
Разбор цепочки «загрузка → распознавание → матч → ревью → раскладка» (действие →
|
||
артефакт в БД) дал естественный набор доменов; приводим capabilities к нему.
|
||
|
||
Ограничение: **рефакторинг только спек, код не трогаем.** Формулировки переносим
|
||
эквивалентными — ни одно нормативное требование не должно измениться по смыслу.
|
||
|
||
## Goals / Non-Goals
|
||
|
||
**Goals:**
|
||
- Одно поведение — один capability; имя читается без знания кода.
|
||
- Полная цепочка представлена в `openspec/specs/` (перенос из `docs/specs/`).
|
||
- Источник истины по мигрируемым темам переезжает в OpenSpec; в `docs/specs/`
|
||
остаётся пометка о переезде (как пилот `ingest`).
|
||
|
||
**Non-Goals:**
|
||
- Никаких изменений поведения, схемы БД, кода, конфигурации.
|
||
- Не вводим новые сущности домена (напр. «тайтл» — отдельная задача беклога).
|
||
- Не дробим метабазу на search/match (решено: одна `metadata-match`).
|
||
- Мульти-бот маршрутизация уведомлений — отдельная задача, здесь только фиксируем
|
||
текущее поведение `notifier`.
|
||
|
||
## Decisions
|
||
|
||
### D1. Механика переноса требований между capabilities
|
||
|
||
OpenSpec 1.4.x поддерживает `RENAMED` только внутри одной спеки. Межспековый
|
||
перенос оформляем парой: **`REMOVED Requirement`** в исходной спеке (с
|
||
`**Reason**`/`**Migration**`, указывающими целевую capability) + **`ADDED
|
||
Requirement`** в целевой. Текст требования в ADDED — эквивалент исходного
|
||
(перенос, не переписывание). Альтернатива (просто RENAMED) не подходит:
|
||
capability меняется, а не имя внутри одной спеки.
|
||
|
||
### D2. `recognition` → `recognition` + `metadata-match`
|
||
|
||
Разные действия: «разобрать сигналы моделью» и «найти/подтвердить запись в базе».
|
||
`Контракт LLM на оригинальное и локализованное названия` **остаётся в
|
||
recognition** (это требование к выходу модели), хотя используют эти поля обе:
|
||
recognition — чтобы модель их заполнила, metadata-match — как ключи поиска.
|
||
|
||
| Текущее требование (recognition) | Назначение |
|
||
|---|---|
|
||
| Сверка с базой по нескольким названиям | → `metadata-match` |
|
||
| Контракт LLM на оригинальное и локализованное названия | остаётся `recognition` |
|
||
| Локаль запроса к TMDB | → `metadata-match` |
|
||
| Нормализация названий при сравнении | → `metadata-match` |
|
||
| Кандидат несёт URL для внешней проверки | → `metadata-match` |
|
||
|
||
Backfill `recognition` из `docs/specs/recognition.md` (ADDED — сейчас в OpenSpec
|
||
этих требований нет):
|
||
- Пред-парс имени релиза (`go-ptn`).
|
||
- Разбор сигналов LLM в структурированный план (схема: `type`, `title`,
|
||
`original_title`, `year`, `provider_hint`, `files[]` с per-file `season`/
|
||
`episode`, `confidence`).
|
||
- Провайдер LLM за абстракцией (`openai-compat`, JSON-mode, ретраи с передачей
|
||
ошибки/схемы; неразобранный после ретраев → review, не failed).
|
||
- Модель уверенности и решение auto/review (авто только при подтверждённом матче
|
||
в базе + структурная валидация + согласованность с пред-парсом).
|
||
- Роли файлов на краях (sample/extra/ignore, привязка внешних субтитров).
|
||
|
||
ADDED в `metadata-match` (сам поиск/подтверждение, backfill из `recognition.md`
|
||
§3, помимо 4 перенесённых):
|
||
- Поиск записи в TMDB/TVDB/TVMaze, сбор кандидатов с дедупом `provider:id`.
|
||
- Подтверждение единичного сильного матча → `provider`/`provider_id`/канон. имя/год.
|
||
- Опциональность баз: нет баз или нет матча → авто-раскладки нет (граница с
|
||
recognition-решением; здесь — что матч не подтверждён).
|
||
|
||
### D3. `review` как отдельный capability
|
||
|
||
Переносим из `web-ui`:
|
||
|
||
| Текущее требование (web-ui) | Назначение |
|
||
|---|---|
|
||
| Единый список источников совпадения на ревью | → `review` |
|
||
| Ручное добавление источника по id или URL | → `review` |
|
||
| Предпросмотр полей источника до фиксации выбора | → `review` |
|
||
|
||
`Матч с записью метабазы ссылкой` **остаётся в web-ui** — это отображение на
|
||
странице загрузки, а не действие ревью.
|
||
|
||
ADDED в `review` (миграция `docs/specs/review-ux.md`):
|
||
- Триггеры входа в review с явной причиной.
|
||
- Команды: Применить, Уточнить (подсказка+перераспознавание), Распознать заново,
|
||
Тип, Игнор файла, Позже, Отклонить, Undo, Привязать заново.
|
||
- Подсказка (мягкая, интерпретирует LLM) vs override (жёсткий пин;
|
||
перераспознавание не затирает).
|
||
- Единый список источников (нейронка наравне с кандидатами) + ручное добавление
|
||
+ предпросмотр «превью = применение» (перенесены из web-ui, п. выше).
|
||
- Разделение труда транспортов (веб — точные правки, Telegram — быстрые действия/
|
||
эскалация в веб); одно состояние ревью, команды сериализует worker.
|
||
|
||
### D4. Миграция `file-layout`, `download-tracking`, `notifications` из docs/specs
|
||
|
||
**`file-layout`** ← `docs/specs/jellyfin-layout.md`:
|
||
- Целевые имена фильмов (`Название (Год) [providerid-…]`).
|
||
- Целевые имена сериалов (папка с provider-id, `Season xx`, `SxxEyy`).
|
||
- Сопоставление источник→цель хардлинками (`save_path`+относит. имя; mkdir 0755).
|
||
- Санитизация целевого имени и запрет выхода за библиотеку.
|
||
- Never-overwrite (тот же inode → готово; другой файл → коллизия → review).
|
||
- Copy-fallback при невозможности хардлинка.
|
||
|
||
**Владение целевым путём** (`superseded`) и **безопасный undo** (`nlink<=1` →
|
||
`ErrLastCopy`) в `file-layout` НЕ дублируем: их доминирующая забота — сверка с
|
||
реальностью (присутствие цели определяется по владению; отказ Undo при пропавшем
|
||
источнике), и они уже живут более полными версиями в `state-reconciliation`.
|
||
`file-layout` описывает акт прямой раскладки; на владение/undo он опирается,
|
||
оставляя их дом в `state-reconciliation` (см. ревью дизайна: устранение дубля).
|
||
|
||
**`download-tracking`** ← `docs/specs/workflow.md` (прямой путь; сверка
|
||
разложенного остаётся в `state-reconciliation`):
|
||
- Поллинг qBittorrent и сопоставление его состояний с нашими.
|
||
- Готовность только когда файлы на месте (не `moving`/`checking*`).
|
||
- Таймауты-предохранители: `metaDL` > `magnet_timeout` → failed; `stalledDL` >
|
||
`stuck_after` → stuck; базис возраста — `added_on` (переживает retry/усыновление).
|
||
- Ошибка qBit (`error`/`missingFiles`) → failed (`qbit_error`).
|
||
- Усыновление раздач по категории **или** тегу, которых нет в БД → `downloading`.
|
||
- Владелец переходов — worker под per-download блокировкой (FSM).
|
||
|
||
**`notifications`** ← `workflow.md` + `state-reconciliation`:
|
||
- Уведомление автора о падении (`failed`/`stuck`), включая приёмный `qbit_add`
|
||
мимо поллинга.
|
||
- Дебаунс повторных падений одной задачи (мерцающий stalled не спамит).
|
||
- Пинг о входе в review и о готовности.
|
||
- Уведомление о рассинхроне (перенос из `state-reconciliation`).
|
||
|
||
| Текущее требование (state-reconciliation) | Назначение |
|
||
|---|---|
|
||
| Уведомление о рассинхроне | → `notifications` |
|
||
| (остальные 11) | остаются `state-reconciliation` |
|
||
|
||
### D5. `identity` → инфраструктура id; приём → `ingest`
|
||
|
||
| Текущее требование (identity) | Назначение |
|
||
|---|---|
|
||
| ULID как первичный ключ сущностей | остаётся `identity` |
|
||
| Нормализация и валидация id на входных границах | остаётся `identity` |
|
||
| Множество инфохэшей загрузки | → `ingest` |
|
||
| Дедупликация приёма по любому из хешей | → `ingest` |
|
||
| Атомарность возврата загрузки в активное состояние | → `ingest` |
|
||
| Корреляция сущностей в логах | остаётся `identity` |
|
||
| Миграция существующих записей | остаётся `identity` |
|
||
|
||
Примечание: `Атомарность возврата в активное состояние` используется и «Привязать
|
||
заново» (review) — но это инвариант приёма/активации загрузки, поэтому его дом —
|
||
`ingest`; review на него ссылается.
|
||
|
||
### D6. Формулировки — эквивалентный перенос
|
||
|
||
При ADDED в целевой capability текст берём из исходного требования (или из
|
||
`docs/specs/` при backfill), сохраняя нормативную силу (SHALL/MUST) и сценарии.
|
||
Правки — только связочные (ссылки на соседние capability), не смысловые. Это
|
||
делает `openspec archive` безопасным: живые спеки после влития эквивалентны сумме
|
||
прежних.
|
||
|
||
## Risks / Trade-offs
|
||
|
||
- **Большой diff, риск потерять требование при переносе** → Перенос по таблицам
|
||
D2–D5 (чек-лист: каждое исходное требование учтено — либо STAY, либо REMOVED+
|
||
ADDED). `openspec validate --strict` + сверка счётчика требований до/после.
|
||
- **Расхождение docs/specs ↔ openspec после переезда** → В `docs/specs/`
|
||
мигрированных файлов ставим шапку «источник истины переехал в
|
||
`openspec/specs/<cap>`»; содержимое не дублируем.
|
||
- **Граница recognition ↔ metadata-match может «поплыть» на будущих задачах**
|
||
(сила совпадения кандидата — идея беклога) → Сейчас фиксируем по действию;
|
||
дальнейшее уточнение — отдельным change.
|
||
- **Пробел: у `ingest` нет формального требования на сам приём** (парс magnet →
|
||
завести download → отдать в qBittorrent) — сейчас поведение не выражено
|
||
требованием ни в одной спеке → см. Open Questions.
|
||
|
||
## Migration Plan
|
||
|
||
1. Дельты change: ADDED в новых/целевых спеках, REMOVED в исходных (D1).
|
||
2. `openspec validate --strict refactor-capability-boundaries`.
|
||
3. Ревью дизайна (этот документ) — **чекпоинт до написания всех дельт**.
|
||
4. После апрува — генерация дельта-спек, повторная валидация.
|
||
5. Пометки о переезде в `docs/specs/` мигрированных файлов; снять пункт беклога.
|
||
6. `openspec archive` — влить дельты в `openspec/specs/`.
|
||
|
||
Откат: change не тронул код; отмена = удалить директорию change (спеки не влиты до
|
||
archive).
|
||
|
||
## Resolved Questions
|
||
|
||
1. **Ядро приёма в `ingest` — ДА.** Добавляем ADDED «Приём источника и заведение
|
||
загрузки» (backfill из `architecture.md` → «Транспорты»): парс magnet → дедуп →
|
||
завести `download`+`download_infohash` → отдать в qBittorrent; ошибка добавления
|
||
→ `failed` (`qbit_add`). Иначе `ingest` остаётся про имя+инфохэши без ядра.
|
||
2. **«Секция раздачи на странице загрузки» — остаётся в `live-status`** (меняется
|
||
вместе с телеметрией).
|
||
3. **Один change** (решение автора). При необходимости порядок дельт разложим на
|
||
этапе apply.
|