Files
avandClaude Opus 4.8 512567c8ba Рефакторинг границ 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>
2026-07-03 21:17:51 +03:00

203 lines
15 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
## 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.