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) явно помечает два класса отклонений от
канона «модуль → репозиторий → таблица»:
- Скелет-репозитории — классы существуют, но все их 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).
- 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, ноль рефактора принятых исключений.