Рефакторинг границ capabilities: цепочка загрузка→матч→ревью→раскладка (openspec)
Привёл набор 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>
This commit is contained in:
@@ -0,0 +1,202 @@
|
||||
## 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.
|
||||
Reference in New Issue
Block a user