Перейти к содержанию

ADR 0018 — Архитектурная карта: осознанные исключения (скелет-репозитории Phase 4 и bypass-писатели)🔗

  • Статус: accepted (2026-06-22)
  • Контекст: продакшенизация «живой карты» (drift-гейт + витрина + скилл); карта вскрыла два класса отклонений от идеала «каждая таблица пишется через репозиторий»
  • Связано: ADR-0008 (Track-3 network puller, Phase-разбивка), docs/architecture/diagrams/seams.md (карта писателей), docs/architecture/arch-map-pilot-run-2026-06-22.md (пилот, где отклонения впервые зафиксированы), PMR-88 (Phase 4 оркестратор)

Контекст🔗

Слой seams живой карты (seams.md) явно помечает два класса отклонений от канона «модуль → репозиторий → таблица»:

  1. Скелет-репозитории — классы существуют, но все их data-методы поднимают NotImplementedError(_PHASE1_SKELETON). Таблица в схеме есть, ничего в неё пока не пишет. Полных скелетов ровно четыре (как и помечено в overview.md и четырьмя ⚠️ в seams.md): SqlAlchemySsThemesRepository, SqlAlchemySsRecommendationsRepository, SqlAlchemySsJudgeResultsRepository, SqlAlchemySsWaveRunsRepository.

Отдельно — несколько частично реализованных репозиториев, где основной путь уже подключён, а отдельные методы отложены в Phase 4 (это НЕ полные скелеты): SsSnapshotsRepository.get_by_snapshot_id, SsPostsRepository.list_for_snapshot, SsPostClassificationsRepository.list_for_snapshot_wave, SsAiReportsRepository (get_latest_weekly_for_candidate, list_for_snapshot).

  1. Bypass-писатели — таблица пишется прямым SQL в обход слоя репозиториев (архитектурный «запах», который карта подсвечивает пунктиром + синим). После рефактора ss_channel_followers_weekly (FR-106, заведён собственный SqlAlchemySsChannelFollowersWeeklyRepository) остаются два осознанных bypass'а: ss_post_tonality (пишется из cli/tonality.py) и weekly_corpus_summary (пишется из слоя services).

Вопрос для продакшенизации: чинить ли их «заодно». Решение пользователя (2026-06-22) — тех-долг закрываем пропорционально: рефакторим только реальную эрозию (ss_channel_followers_weekly), а остальное — осознанно оставляем и документируем, чтобы карта не зеленела ложно и чтобы будущий читатель не принял эти отклонения за недосмотр.

Решение🔗

Р1. Скелет-репозитории — это осознанный Phase 4, не недоделка🔗

Скелеты не реализуем в этой инициативе. Они — запланированный задел под Phase 4 (Wave A/B/C orchestrator, PMR-88): класс и сигнатуры зафиксированы, тело осознанно поднимает NotImplementedError, пока оркестратор-писатель не подключён. Карта помечает их «writer pending» (серый пунктир) — это корректное, а не тревожное состояние.

Критерий выхода из скелета: соответствующий Phase-4 путь записи (оркестратор волны) реализован и покрыт интеграционным тестом round-trip; тогда NotImplementedError снимается и seam становится сплошной стрелкой.

Р2. Два bypass-писателя приняты как исключения — с критериями🔗

ss_post_tonality и weekly_corpus_summary оставляем на прямом SQL. Допустимость определяется тремя критериями (исключение валидно, пока выполнен хотя бы релевантный из них):

  • Одноразовый/батчевый ingest, не доменный write-path. Запись идёт из конечного CLI-/сервис-узла одним батчем за прогон, а не из доменной логики, которую переиспользуют разные вызывающие. Репозиторий не дал бы переиспользования — только лишний слой.
  • Сервис владеет вычислением, а не только персистентом. weekly_corpus_summary — материализованный результат расчёта внутри services; таблица здесь «кэш вычисления», а не доменная сущность с независимым жизненным циклом.
  • Идемпотентность гарантируется БД, не слоем. Запись идёт через ON CONFLICT … DO UPDATE/NOTHING; корректность повтора обеспечена ограничением таблицы, поэтому отсутствие репозитория не создаёт риска рассинхрона.

Критерий пересмотра (когда исключение перестаёт быть валидным и требует рефактора в репозиторий): у таблицы появляется второй независимый писатель ИЛИ запись переезжает в доменную логику с несколькими вызывающими ИЛИ нужна транзакционная композиция с другими репозиториями в одном session_scope. Тогда — завести репозиторий по образцу SqlAlchemySsChannelFollowersWeeklyRepository (инъецируемая сессия, без commit внутри; commit — в композиционном корне, гранулярность «один на батч»).

Р3. Почему ss_channel_followers_weekly — исключение из исключений (зачем его-то рефакторили)🔗

В отличие от Р2, у followers-таблицы было два пути записи (ingest из cli/socialscan.py и backfill) плюс независимый путь чтения (reports/track2/snapshot_builder.py) — то есть реальная эрозия с риском рассинхрона семантики между писателями. Это ровно тот триггер пересмотра из Р2, поэтому followers рефакторили в репозиторий, а tonality/weekly_corpus — нет.

Последствия🔗

  • Карта (seams) остаётся честной: скелеты — серый пунктир «writer pending», два bypass'а — синий пунктир «осознанное исключение», followers — сплошная стрелка через репозиторий. Drift-гейт проверяет присутствие всех 34 таблиц манифеста в схеме, но не навязывает «всё через репозиторий» — этот ADR фиксирует, где канон осознанно не применяется.
  • Будущий контрибьютор имеет критерии, а не догадки: когда скелет пора реализовать (Phase 4 round-trip готов) и когда bypass пора заводить в репозиторий (второй писатель / доменный write-path / транзакционная композиция).
  • Объём инициативы остаётся пропорциональным: ноль спекулятивного кода под Phase 4, ноль рефактора принятых исключений.