diff --git a/plans/2026-07-05-render-pipeline-refactor.md b/plans/2026-07-05-render-pipeline-refactor.md index 0d51711..0009d36 100644 --- a/plans/2026-07-05-render-pipeline-refactor.md +++ b/plans/2026-07-05-render-pipeline-refactor.md @@ -1,8 +1,10 @@ -# Рефакторинг пайплайна рендера: спецификация (v5) +# Рефакторинг пайплайна рендера: спецификация (v6) Статус: пять раундов адверсариального ревью — четыре у постоянного критика + независимый холодный проход (журнал в §8), блокеров нет. Холодный проход независимо подтвердил все line-refs и эмпирические утверждения спеки. +v6: golden-корпус переведён с синтетики на записанные реальные сообщения +(решение владельца, обоснование в этапе 0 и §8). База: ветка `refactor/render-pipeline` режется от **f9550d8** (`feat/kurigram-2.2.23`); все line-refs спеки действительны для этого коммита. Мёрж рефакторинга в main блокируется решением по kurigram-PR. @@ -150,20 +152,62 @@ Golden-эталоны (§5, этап 0) обновляются только ко ### Этап 0 — golden-эталоны фидов (до любых правок кода) -Фикстурный набор синтетических сообщений (naive-даты): одиночный пост, -media_group, time-based кластер, merged-группа с несколькими текстами, poll, -webpage-превью с фото и без, forward, реакции (обычные / custom / paid / -пустой список), текст с зачёркиванием (``/`` через entities — без -такой фикстуры §3.1 в golden невидим). +**Корпус — записанные РЕАЛЬНЫЕ сообщения с прод-сервера, не синтетика (v6).** +Источник: рабочий кэш бриджа `data/tgcache/` на сервере — там уже лежат +пиклы ровно нужного формата: `{channel}.cache` = +`{'timestamp', 'limit', 'messages': List[Message]}` +(`tg_cache._save_history_to_cache`) и `{channel}.chatinfo` (для +`cached_get_chat`). Отобранные ПАРЫ файлов копируются с сервера в +`tests/test_data/recorded/` и коммитятся как замороженный корпус. Состав +(какие каналы) выбирает владелец — контент этих каналов попадает в репо. -Сценарии exclude_flags/exclude_text в golden-фикстуры НЕ входят: фильтры не -меняют байты выживших постов; их семантика (membership / regex) закрепляется -парой unit-тестов — дешевле и меньше площадь флаки-рисков. +Реплей: тестовый загрузчик распикливает файлы напрямую (`timestamp`/`limit` +игнорируются — никакой проверки свежести) и monkeypatch'ит +`tg_cache.cached_get_chat_history` / `cached_get_chat` на возврат записанных +объектов. Это буквально прод-путь cache-hit: сериализация и структура +объектов — те же, что видит рендер в бою. -**None-date фикстура в этап 0 НЕ входит**: текущий код падает на ней в -ДЕФОЛТНОМ пути (naive/aware TypeError в sort-ключе групп, см. §1) — снять -golden невозможно. Она добавляется в golden-набор коммитом этапа 4 со ссылкой -на §3.11–12 (это ДОБАВЛЕНИЕ фикстуры, а не обновление эталона). +Почему реальные, а не моки: (а) настоящие kurigram-объекты закрывают класс +ложной зелени «мок и код согласованно ждут атрибут, которого нет у реального +объекта» — холодный проход (§8, раунд 5) пометил его как неловимый на моках; +(б) naive-даты, реальные entities/reactions/webpage-структуры и комбинации +форматирования достаются бесплатно. + +**Первое действие этапа — проверка совместимости пиклов**: кэш сервера +записан прод-версией kurigram, корпус распикливается под 2.2.23 (база +рефакторинга f9550d8). ВЫПОЛНЕНО — см. «Статус» ниже: 90/90 + 89/89 без +ошибок, фолбэк не понадобился. + +**Инвентаризация покрытия**: мини-скрипт проходит по корпусу и печатает +media-типы, наличие media_groups / time-кластеров / forward / reply / +реакций (обычные, custom, paid, пустой объект) / webpage с фото и без / +зачёркивания (``/`` в entities). Дыры покрытия закрываются +СИНТЕТИЧЕСКОЙ добавкой — только для недостижимого в записанном. + +**Статус (2026-07-05): оба шага уже выполнены.** Снапшот прод-кэша снят: +`data/tgcache_prod_2026-07-05/` (90 `.cache` + 89 `.chatinfo`, 53 МБ, вне +git — `data/` в .gitignore; отбор пар в `tests/test_data/recorded/` — задача +этапа 0). Совместимость подтверждена: ВСЕ файлы распикливаются под kurigram +2.2.23 без ошибок. Инвентарь (8582 сообщения): PHOTO 5397, text-only 1609, +WEB_PAGE 764 (с фото 581 / без 183), VIDEO 653, DOCUMENT 51, ANIMATION 33, +POLL 31, VIDEO_NOTE 15, STICKER 13, AUDIO 8, VOICE 6, GIVEAWAY и +GIVEAWAY_WINNERS по 1; media_group-членов 3159, forward 670, reply 357, +time-кластерных пар (gap≤5s) ~294; реакции: обычных 17981, custom 1018, +paid 593, ПУСТЫХ reactions-объектов 17 (§3.15-edge покрыт реальными +данными!); зачёркиваний 150 (§3.1 покрыт); все 8582 даты naive (посылка +спеки подтверждена); None-date — 0. Синтетика нужна ТОЛЬКО для: None-date +(этап 4) и отсутствующих в корпусе типов — PAID_MEDIA, STORY, LIVE_PHOTO, +CHECKLIST, CONTACT/LOCATION/VENUE/DICE/GAME/INVOICE/UNSUPPORTED +(fragment-уровень этапа 5a). + +Сценарии exclude_flags/exclude_text в golden НЕ входят: фильтры не меняют +байты выживших постов; их семантика (membership / regex) закрепляется парой +unit-тестов — дешевле и меньше площадь флаки-рисков. + +**None-date пост в этап 0 НЕ входит** (в записанных данных его и не бывает): +текущий код падает на нём в ДЕФОЛТНОМ пути (naive/aware TypeError в +sort-ключе групп, см. §1). Синтетическая None-date фикстура добавляется в +HTML-golden коммитом этапа 4 со ссылкой на §3.11–12. Снимаются и кладутся в `tests/test_data/golden/`: @@ -181,7 +225,7 @@ golden невозможно. Она добавляется в golden-набор прогонами) вырезается regex'ом и в golden, и в актуальном выводе; `` в feedgen 1.0.0 версии НЕ содержит — его нормализация не обязательна, оставлена как дешёвая страховка от апгрейда библиотеки; -- фикстуры не содержат постов, где `pubDate` берётся из `datetime.now()` +- корпус не содержит постов, где `pubDate` берётся из `datetime.now()` (это только None-date случай — он исключён, см. выше); - порядок merged-флагов ДО этапа 2 недетерминирован: `list(set(...))` (rss_generator.py:260–265) зависит от PYTHONHASHSEED процесса, который из @@ -193,10 +237,10 @@ golden невозможно. Она добавляется в golden-набор tests/test_new_media_types.py): иначе golden, снятый на dev-машине, краснеет на CI — свежий checkout генерирует новый `secrets.token_hex` в `data/media_digest.key`, и все digest'ы в URL меняются; -- фикстуры подаются monkeypatch'ем `tg_cache.cached_get_chat` / - `cached_get_chat_history` — lazy-import внутри фид-функций разрешает имена - поздно, поэтому патч модуля tg_cache работает (образец — - test_stage4_eventloop.py); +- корпус подаётся monkeypatch'ем `tg_cache.cached_get_chat` / + `cached_get_chat_history` (возврат распикленных записанных объектов) — + lazy-import внутри фид-функций разрешает имена поздно, поэтому патч модуля + tg_cache работает (образец — test_stage4_eventloop.py); - запись media-id пинится: monkeypatch `upsert_media_file_ids_bulk_sync` (или `DB_PATH` → tmp_path) — иначе golden-тесты с медиа-фикстурами пишут в реальную `./data/media_file_ids.db` (`DB_PATH` cwd-относителен, @@ -218,8 +262,11 @@ sanitizer), этап 2 (пп.6, 8, 15 + снятие нормализации ф webpage-без-фото меняет и feed-level байты: до 5b незакрытый div дозакрывался bleach'ем в конце фрагмента). -DoD: снапшоты в репо, тест сравнения зелёный и детерминированный (два прогона -подряд — идентичные байты), любое изменение рендера валит его с внятным диффом. +DoD: записанный корпус + отчёт инвентаризации + снапшоты в репо; тест +сравнения зелёный и детерминированный (два прогона подряд — идентичные +байты); любое изменение рендера валит его с внятным диффом. Пиклы корпуса +сцеплены с версией kurigram — при апгрейде библиотеки корпус перезаписывается +с сервера (зафиксировать в README тестов). ### Этап 1 — `sanitizer.py` + санитайз в пайплайне (ишью #22, обновить) @@ -457,6 +504,13 @@ PAID_MEDIA, WEB_PAGE с фото (пустой media-div!), **и edge-ветки без фото (незакрытый div — воспроизводится в 5a буквально), file_unique_id is None, channel_username is None, гейт webpage-превью «text ≤ 10». +Источник сообщений для fragment-снапшотов (v6): ЗАПИСАННЫЙ КОРПУС этапа 0 — +для всех типов, которые в нём есть (по инвентарю: PHOTO, VIDEO, ANIMATION, +VIDEO_NOTE, AUDIO, VOICE, STICKER, DOCUMENT, POLL, WEB_PAGE, GIVEAWAY, +GIVEAWAY_WINNERS). Моки — ТОЛЬКО для отсутствующих в корпусе типов +(PAID_MEDIA, STORY, LIVE_PHOTO, CHECKLIST и прочая экзотика) — ограничение +инвариант-теста «ложная зелень на моках» сужается до этих типов. + ```python # Single source of truth: media type -> (object selector, render kind). # kind=None means "selector-only entry": the object participates in file-id @@ -694,3 +748,23 @@ find_file_id_in_message); архитектурные решения призна каветт §3.12 про будущие даты. Отклонённых претензий нет; по всем спорным пунктам достигнут консенсус. + +### v6 — golden-корпус: записанные реальные сообщения (решение владельца) + +Не раунд ревью — изменение дизайна по решению владельца проекта, с +немедленной эмпирической проверкой: + +- Корпус этапа 0 переведён с синтетики на записанные кэши прод-сервера + (`data/tgcache/*.cache|.chatinfo` — пиклы того же формата, что читает + прод-путь cache-hit). Мотив: холодный проход (раунд 5) пометил класс + «мок и код согласованно ждут несуществующий атрибут» как неловимый на + моках — реальные объекты закрывают его целиком. +- Снапшот кэша снят (90 каналов), совместимость пиклов со старым kurigram + проверена под 2.2.23: 179/179 файлов без ошибок. +- Инвентарь покрытия: 8582 сообщения, все даты naive (посылка спеки + подтверждена данными); покрыты в т.ч. edge-кейсы §3.1 (зачёркивания: 150) + и §3.15 (пустые reactions-объекты: 17), webpage с/без фото, ~294 + time-кластерные пары. Синтетика сузилась до None-date и отсутствующих + типов (PAID_MEDIA, STORY, LIVE_PHOTO, CHECKLIST, прочая экзотика). +- Fragment-снапшоты этапа 5a — тоже на корпусе, где тип доступен; + ограничение инвариант-теста сузилось до mock-only типов.