Привёл набор 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>
15 KiB
Context
Набор capabilities в openspec/specs/ сложился по ходу пилотной миграции и не
отражает цепочку обработки загрузки. Три проблемы:
recognitionсмешивает разбор LLM и работу с метабазами.- Поведение ревью размазано: часть — в
web-ui, основная часть — в не перенесённомdocs/specs/review-ux.md. - Звенья цепочки
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-fileseason/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
- Дельты change: ADDED в новых/целевых спеках, REMOVED в исходных (D1).
openspec validate --strict refactor-capability-boundaries.- Ревью дизайна (этот документ) — чекпоинт до написания всех дельт.
- После апрува — генерация дельта-спек, повторная валидация.
- Пометки о переезде в
docs/specs/мигрированных файлов; снять пункт беклога. openspec archive— влить дельты вopenspec/specs/.
Откат: change не тронул код; отмена = удалить директорию change (спеки не влиты до archive).
Resolved Questions
- Ядро приёма в
ingest— ДА. Добавляем ADDED «Приём источника и заведение загрузки» (backfill изarchitecture.md→ «Транспорты»): парс magnet → дедуп → завестиdownload+download_infohash→ отдать в qBittorrent; ошибка добавления →failed(qbit_add). Иначеingestостаётся про имя+инфохэши без ядра. - «Секция раздачи на странице загрузки» — остаётся в
live-status(меняется вместе с телеметрией). - Один change (решение автора). При необходимости порядок дельт разложим на этапе apply.