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

15 KiB
Raw Permalink Blame History

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. recognitionrecognition + 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-layoutdocs/specs/jellyfin-layout.md:

  • Целевые имена фильмов (Название (Год) [providerid-…]).
  • Целевые имена сериалов (папка с provider-id, Season xx, SxxEyy).
  • Сопоставление источник→цель хардлинками (save_path+относит. имя; mkdir 0755).
  • Санитизация целевого имени и запрет выхода за библиотеку.
  • Never-overwrite (тот же inode → готово; другой файл → коллизия → review).
  • Copy-fallback при невозможности хардлинка.

Владение целевым путём (superseded) и безопасный undo (nlink<=1ErrLastCopy) в file-layout НЕ дублируем: их доминирующая забота — сверка с реальностью (присутствие цели определяется по владению; отказ Undo при пропавшем источнике), и они уже живут более полными версиями в state-reconciliation. file-layout описывает акт прямой раскладки; на владение/undo он опирается, оставляя их дом в state-reconciliation (см. ревью дизайна: устранение дубля).

download-trackingdocs/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).

notificationsworkflow.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.