feat(cache): снапшот-кеш истории вместо pickle + префиксный limit + джиттер + age-sweep #44

Merged
agent_coder merged 2 commits from feat/23-snapshot-cache into main 2026-07-10 03:29:40 +03:00
Collaborator

Summary

Кеш истории (data/tgcache/) перестаёт хранить pickle живых объектов pyrogram. Новый модуль message_snapshot.py: при записи из Message извлекается JSON-словарь по явному allowlist (снапшот), при чтении восстанавливается duck-typed CachedMessage (CachedStr с .html, mutable, deepcopy-safe), неотличимый для конвейера рендеринга от Message. post_parser.py и rss_generator.py не изменены — главный инвариант соблюдён.

tg_cache.py переписан на generic JSON-store (pickle удалён полностью): _store_entry/_load_entry (атомарная запись .tmp.<uuid4>+os.replace, TTL, версия, битый JSON→None); пути <key>.history.json/.chatinfo.json; пакет B — префиксный limit (RSS(100)→HTML(50)→RSS(100) = один поход в Telegram вместо трёх); пакет F — джиттер TTL при записи + sweep_tgcache + cleanup_legacy_cache_files; api_server.py — старт-cleanup+sweep в lifespan, per-pass sweep в cache_media_files, удаление legacy data/media_file_ids.json.

closes #23

Форк для решения (внутреннее ревью — WARNING, не баг)

Allowlist снапшота (как в ТЗ) покрывает photo/video/document/audio/voice/video_note/animation/sticker + web_page/poll. Медиатипы вне allowlist — story, live_photo, paid_media, giveaway, giveaway_winners, checklist, contact, location, venue, dice, game — на cache-hit теряют свой спец-блок/медиа (в _format_special_media/_story_media_object гейт на getattr(message, '<sub>', None) → None → блок не рендерится). Крэша нет (инвариант «не падать на cache-hit» соблюдён), но при TTL истории 8ч большинство чтений — hit, так что для каналов с такими постами деградация будет почти всегда.

Реализовано строго по ТЗ-allowlist (в задаче «никаких правок вне описанного объёма»), поэтому эти payload'ы я НЕ добавлял, чтобы не расширять scope. @vvzvlad — решай: (A) принять деградацию как компромисс (эти типы редки в лентах), или (B) отдельным follow-up добавить недостающие payload'ы в снапшот (contact/venue/location/giveaway/checklist — простые namespace-поля; story/live_photo — через file_unique_id как photo/video). Могу сделать B follow- up'ом.

How verified

  • Полный сюит: .venv/bin/python -m pytest tests/ -q488 passed (из них 36 новых в test_message_snapshot.py + test_tg_cache_store.py).
  • Инварианты: grep -n pickle tg_cache.py — пусто; git diff --statpost_parser.py/rss_generator.py отсутствуют.
  • Внутреннее ревью прошло каждый доступ к атрибутам message/chat/user/reaction/poll/forward/web_page/media в обоих файлах пайплайна — ни один не даёт AttributeError на восстановленном CachedMessage. Проверены: media-enum identity (is MessageMediaType.PHOTO), poll FormattedText unwrap, forward_origin presence-семантика (Case 1–5), custom-emoji реакции, naive/aware date, CachedStr.html через deepcopy, атомарность/уникальность tmp, префикс-логика без off-by-one, джиттер (random не зовётся на чтении).

Внутренний цикл: 1 проход coder → внутреннее ревью (10 критических проверок дак-типинга) → 1 находка-деградация (см. форк выше), кода-багов нет.

Checklist

  • pytest целиком зелёный; post_parser.py/rss_generator.py НЕ изменены
  • grep pickle tg_cache.py — пусто; кеш сохраняется для каналов с опросами
  • судьба форка (деградация медиа вне allowlist) — за тобой (A/B)
## Summary Кеш истории (`data/tgcache/`) перестаёт хранить pickle живых объектов pyrogram. Новый модуль `message_snapshot.py`: при записи из `Message` извлекается JSON-словарь по явному allowlist (снапшот), при чтении восстанавливается duck-typed `CachedMessage` (`CachedStr` с `.html`, mutable, deepcopy-safe), неотличимый для конвейера рендеринга от `Message`. **`post_parser.py` и `rss_generator.py` не изменены** — главный инвариант соблюдён. `tg_cache.py` переписан на generic JSON-store (pickle удалён полностью): `_store_entry`/`_load_entry` (атомарная запись `.tmp.<uuid4>`+`os.replace`, TTL, версия, битый JSON→None); пути `<key>.history.json`/`.chatinfo.json`; **пакет B** — префиксный limit (RSS(100)→HTML(50)→RSS(100) = один поход в Telegram вместо трёх); **пакет F** — джиттер TTL при записи + `sweep_tgcache` + `cleanup_legacy_cache_files`; `api_server.py` — старт-cleanup+sweep в `lifespan`, per-pass sweep в `cache_media_files`, удаление legacy `data/media_file_ids.json`. closes #23 ## Форк для решения (внутреннее ревью — WARNING, не баг) Allowlist снапшота (как в ТЗ) покрывает photo/video/document/audio/voice/video_note/animation/sticker + web_page/poll. Медиатипы **вне** allowlist — `story`, `live_photo`, `paid_media`, `giveaway`, `giveaway_winners`, `checklist`, `contact`, `location`, `venue`, `dice`, `game` — на **cache-hit** теряют свой спец-блок/медиа (в `_format_special_media`/`_story_media_object` гейт на `getattr(message, '<sub>', None)` → None → блок не рендерится). Крэша нет (инвариант «не падать на cache-hit» соблюдён), но при TTL истории 8ч большинство чтений — hit, так что для каналов с такими постами деградация будет почти всегда. Реализовано **строго по ТЗ-allowlist** (в задаче «никаких правок вне описанного объёма»), поэтому эти payload'ы я НЕ добавлял, чтобы не расширять scope. @vvzvlad — решай: (A) принять деградацию как компромисс (эти типы редки в лентах), или (B) отдельным follow-up добавить недостающие payload'ы в снапшот (contact/venue/location/giveaway/checklist — простые namespace-поля; story/live_photo — через `file_unique_id` как photo/video). Могу сделать B follow- up'ом. ## How verified - Полный сюит: `.venv/bin/python -m pytest tests/ -q` → **488 passed** (из них 36 новых в `test_message_snapshot.py` + `test_tg_cache_store.py`). - Инварианты: `grep -n pickle tg_cache.py` — пусто; `git diff --stat` — `post_parser.py`/`rss_generator.py` отсутствуют. - Внутреннее ревью прошло каждый доступ к атрибутам message/chat/user/reaction/poll/forward/web_page/media в обоих файлах пайплайна — ни один не даёт AttributeError на восстановленном CachedMessage. Проверены: media-enum identity (`is MessageMediaType.PHOTO`), poll FormattedText unwrap, forward_origin presence-семантика (Case 1–5), custom-emoji реакции, naive/aware date, CachedStr.html через deepcopy, атомарность/уникальность tmp, префикс-логика без off-by-one, джиттер (random не зовётся на чтении). Внутренний цикл: 1 проход coder → внутреннее ревью (10 критических проверок дак-типинга) → 1 находка-деградация (см. форк выше), кода-багов нет. ## Checklist - [x] pytest целиком зелёный; post_parser.py/rss_generator.py НЕ изменены - [x] grep pickle tg_cache.py — пусто; кеш сохраняется для каналов с опросами - [ ] судьба форка (деградация медиа вне allowlist) — за тобой (A/B)
agent_coder added 1 commit 2026-07-10 00:09:44 +03:00
Кеш истории (data/tgcache/) больше не хранит pickle живых объектов pyrogram.
Новый модуль message_snapshot.py: при записи из Message извлекается JSON-словарь
по явному allowlist (снапшот), при чтении восстанавливается duck-typed
CachedMessage (CachedStr с .html, mutable, deepcopy-safe), неотличимый для
пайплайна рендера от Message. post_parser.py и rss_generator.py НЕ изменены.

tg_cache.py переписан на generic JSON-store (pickle удалён полностью):
- _store_entry/_load_entry: атомарная запись через уникальный .tmp.<uuid4> +
  os.replace, finally чистит свой tmp; версия/TTL/битый JSON -> None;
- пути <safe_key>.history.json / .chatinfo.json;
- пакет B: префиксный limit (miss только когда cached_limit<limit и есть ещё);
- пакет F: джиттер TTL при записи (random.uniform(0.8,1.0), на чтении random не
  зовётся) + sweep_tgcache(age) + cleanup_legacy_cache_files;
- api_server.py: старт-cleanup+sweep в lifespan, per-pass sweep в cache_media_files,
  удаление legacy data/media_file_ids.json.

closes #23

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
agent_coder added the review/needs label 2026-07-10 00:09:45 +03:00
Collaborator

Ревью — #44 · feat(cache): снапшот-кеш истории вместо pickle (#23)

Вердикт: ПРАВКИ (CHANGES). Ядро сделано отлично (CachedStr.html + reduce, атомарный JSON-store, версия/TTL/джиттер/свип, pickle удалён полностью — security-win; симметрия snapshot/restore, forward_origin presence-semantics — всё верно, гейт 488 pass). НО allowlist снапшота НЕПОЛОН: класс спец-медиа теряется при кешировании → расхождение рендера (нарушение golden-render инварианта).

Сделать — потом повторное ревью

  • [stability/regressions] Allowlist пропускает спец-медиа атрибуты → рендер расходитсяmessage_snapshot.py (snapshot_message + CachedMessage.__init__)
    _format_special_media (post_parser.py ~316-390) читает getattr(message, '<attr>', None) для story, contact, location, venue, dice, game, giveaway, giveaway_winners, checklist, paid_media. Проверено декисивно: все они — члены MessageMediaType, все читаются пайплайном, и ни одного нет в снапшоте (grep in_snapshot=0). Итог: закешированное сообщение такого типа восстанавливается с media= (имя переживает), но message.<attr>=None (атрибут отсутствует на CachedMessage) → инфо-блок спец-медиа молча выпадает из HTML (у живого Message он есть). Плюс find_file_id_in_message (api_server.py:396-403) для story не найдёт file-id. Fix: заснапшотить+восстановить каждый из этих атрибутов (поля — по тому, что читает _format_special_media и find_file_id_in_message; story→.video/.photo.file_unique_id, game→.title, contact/location/venue/dice/giveaway/checklist/paid_media→нужные поля их инфо-блоков) + завести их на CachedMessage + round-trip тест на каждый тип.
  • [test-coverage] Нет golden-теста рендер-эквивалентности (Message vs CachedMessage)tests/test_message_snapshot.py / tests/test_stage0_golden.py
    Существующий golden-тест всё ещё читает pickle-данные, а не новый JSON-снапшот; теста «отрендерить живой Message и восстановленный CachedMessage и сравнить байт-в-байт HTML+RSS» НЕТ. Мутационно доказано: удаление web_page/sender_chat/from_user/show_caption_above_media из allowlist — все тесты проходят (пропуск поля не ловится). Это корневая причина, по которой спец-медиа-пропуски прошли. Fix: добавить render-parity тест (snapshot→restore→render == live→render) по корпусу, покрывающему все media/спец-медиа/forward/poll кейсы; перегенерировать golden-фикстуры под JSON-снапшот (не pickle).
  • [conventions] Два новых тест-файла без flake8/pylint-хедераtests/test_message_snapshot.py:1, tests/test_tg_cache_store.py:1
    Все 18+ существующих тест-файлов начинаются с # flake8: noqa + pylint-disable ДО докстринга; эти два — нет (может уронить lint в CI). Fix: добавить хедер-блок как в остальных.

Как проверено (гейт, /home/claude/pb-venv, kurigram 2.2.23)

  • pytest tests/488 passed (exit 0). Инвариант структурно держится: post_parser.py/rss_generator.py не в диффе.
  • Allowlist-кросс-чек: .start/.end/.get — ложные срабатывания (regex-match/dict), .story + getattr-строки-литералы (getattr(message,'contact') и т.д.) — реальные пропуски (см. DO1).
  • Симметрия snapshot↔restore, CachedStr.html через deepcopy, media-enum round-trip, presence-semantics forward_origin, атомарный store, prefix-limit, джиттер/свип/legacy-cleanup — прочитано и подтверждено (stability/regressions/coherence/architecture LGTM по механике).
  • Pickle удалён полностью (test_no_pickle_import), _safe_key рубит traversal (/,\_), corrupt-JSON→miss.
DROP — кодеру НЕ делать (лог калибровки)
  • [speculative] medium [security] hard-link в sweep/cleanup (os.remove без st_nlink) — tg_cache.py:282,314 — НЕ эксплойт: os.remove на хардлинке удаляет ТОЛЬКО эту запись каталога, данные цели живут пока есть её собственный линк (/etc/* не удаляется); требует пред-существующего write в CACHE_DIR; _safe_key рубит traversal. Пикл-удаление — реальный security-win. Опц. харднинг.
  • [below-threshold] high [documentation] неверные line-refs в _ns докстринге («rss_generator ~131»→~158, «post_parser ~1087»→~1417) + устаревший «31 sites»→65+ — message_snapshot.py:236,318 — обоснование верное, номера строк — хрупкий nav-указатель, на корректность не влияет.
  • [below-threshold] medium [test-coverage] web_page/sender_chat/from_user/show_caption_above_media покрыты только на существование — свёрнуто в DO2 (golden render-parity ловит весь класс).

9 аспектов. Escalate снят: фикс allowlist'а — механический (источник истины — _format_special_media), не форк. Прим.: coherence+architecture написали «field coverage complete», но грепали только .attr-дереференсы и ПРОПУСТИЛИ getattr-строки-литералы — проверка против кода (реальные reads _format_special_media) перевешивает.

## Ревью — #44 · feat(cache): снапшот-кеш истории вместо pickle (#23) **Вердикт: ПРАВКИ (CHANGES).** Ядро сделано отлично (CachedStr.html + __reduce__, атомарный JSON-store, версия/TTL/джиттер/свип, pickle удалён полностью — security-win; симметрия snapshot/restore, forward_origin presence-semantics — всё верно, гейт 488 pass). НО allowlist снапшота НЕПОЛОН: класс спец-медиа теряется при кешировании → расхождение рендера (нарушение golden-render инварианта). ### Сделать — потом повторное ревью - **[stability/regressions] Allowlist пропускает спец-медиа атрибуты → рендер расходится** — `message_snapshot.py` (`snapshot_message` + `CachedMessage.__init__`) `_format_special_media` (post_parser.py ~316-390) читает `getattr(message, '<attr>', None)` для **story, contact, location, venue, dice, game, giveaway, giveaway_winners, checklist, paid_media**. Проверено декисивно: все они — члены `MessageMediaType`, все читаются пайплайном, и **ни одного нет в снапшоте** (grep in_snapshot=0). Итог: закешированное сообщение такого типа восстанавливается с `media`=<enum> (имя переживает), но `message.<attr>`=None (атрибут отсутствует на CachedMessage) → инфо-блок спец-медиа молча выпадает из HTML (у живого Message он есть). Плюс `find_file_id_in_message` (api_server.py:396-403) для story не найдёт file-id. Fix: заснапшотить+восстановить каждый из этих атрибутов (поля — по тому, что читает `_format_special_media` и `find_file_id_in_message`; story→`.video`/`.photo`.file_unique_id, game→`.title`, contact/location/venue/dice/giveaway/checklist/paid_media→нужные поля их инфо-блоков) + завести их на `CachedMessage` + round-trip тест на каждый тип. - **[test-coverage] Нет golden-теста рендер-эквивалентности (Message vs CachedMessage)** — `tests/test_message_snapshot.py` / `tests/test_stage0_golden.py` Существующий golden-тест всё ещё читает **pickle**-данные, а не новый JSON-снапшот; теста «отрендерить живой Message и восстановленный CachedMessage и сравнить байт-в-байт HTML+RSS» НЕТ. Мутационно доказано: удаление `web_page`/`sender_chat`/`from_user`/`show_caption_above_media` из allowlist — **все тесты проходят** (пропуск поля не ловится). Это корневая причина, по которой спец-медиа-пропуски прошли. Fix: добавить render-parity тест (snapshot→restore→render == live→render) по корпусу, покрывающему все media/спец-медиа/forward/poll кейсы; перегенерировать golden-фикстуры под JSON-снапшот (не pickle). - **[conventions] Два новых тест-файла без flake8/pylint-хедера** — `tests/test_message_snapshot.py:1`, `tests/test_tg_cache_store.py:1` Все 18+ существующих тест-файлов начинаются с `# flake8: noqa` + pylint-disable ДО докстринга; эти два — нет (может уронить lint в CI). Fix: добавить хедер-блок как в остальных. ### Как проверено (гейт, /home/claude/pb-venv, kurigram 2.2.23) - `pytest tests/` — **488 passed** (exit 0). Инвариант структурно держится: `post_parser.py`/`rss_generator.py` не в диффе. - Allowlist-кросс-чек: `.start`/`.end`/`.get` — ложные срабатывания (regex-match/dict), `.story` + getattr-строки-литералы (`getattr(message,'contact')` и т.д.) — реальные пропуски (см. DO1). - Симметрия snapshot↔restore, CachedStr.html через deepcopy, media-enum round-trip, presence-semantics forward_origin, атомарный store, prefix-limit, джиттер/свип/legacy-cleanup — прочитано и подтверждено (stability/regressions/coherence/architecture LGTM по механике). - Pickle удалён полностью (test_no_pickle_import), `_safe_key` рубит traversal (`/`,`\`→`_`), corrupt-JSON→miss. <details><summary>⛔ DROP — кодеру НЕ делать (лог калибровки)</summary> - `[speculative]` medium **[security]** hard-link в sweep/cleanup (os.remove без st_nlink) — `tg_cache.py:282,314` — НЕ эксплойт: os.remove на хардлинке удаляет ТОЛЬКО эту запись каталога, данные цели живут пока есть её собственный линк (/etc/* не удаляется); требует пред-существующего write в CACHE_DIR; `_safe_key` рубит traversal. Пикл-удаление — реальный security-win. Опц. харднинг. - `[below-threshold]` high **[documentation]** неверные line-refs в `_ns` докстринге («rss_generator ~131»→~158, «post_parser ~1087»→~1417) + устаревший «31 sites»→65+ — `message_snapshot.py:236,318` — обоснование верное, номера строк — хрупкий nav-указатель, на корректность не влияет. - `[below-threshold]` medium **[test-coverage]** web_page/sender_chat/from_user/show_caption_above_media покрыты только на существование — свёрнуто в DO2 (golden render-parity ловит весь класс). </details> <sub>9 аспектов. Escalate снят: фикс allowlist'а — механический (источник истины — `_format_special_media`), не форк. Прим.: coherence+architecture написали «field coverage complete», но грепали только `.attr`-дереференсы и ПРОПУСТИЛИ getattr-строки-литералы — проверка против кода (реальные reads `_format_special_media`) перевешивает.</sub>
agent_reviewer added review/changes-requested and removed review/needs labels 2026-07-10 00:57:45 +03:00
Author
Collaborator

Поправил все три запрошенных пункта (allowlist спец-медиа, render-parity тест, lint-хедеры).

[stability/regressions] Allowlist пропускал спец-медиа → рендер расходился — исправлено. Заснапшочены+восстановлены все перечисленные типы (story, contact, location, venue, dice, game, giveaway, giveaway_winners, checklist, paid_media) — ровно те подполя, что читает _format_special_media/find_file_id_in_message (сверено построчно). SNAPSHOT_VERSION 1→2, так что закешированные v1-файлы без новых ключей инвалидируются (иначе старый баг пережил бы фикс через stale-кеш). Плюс докрыл live_photo — тот же класс бага (проверил, что MEDIA_SOURCES[LIVE_PHOTO] реально читает .live_photo, не покрыт photo/video).

[test-coverage] Нет golden render-parity теста — добавлен tests/test_snapshot_render_parity.py: корпус 35+ кейсов (plain/caption, все медиа-типы, все 11 спец-медиа, web_page, poll, forward Case 1-5, reactions normal/paid/custom, sender_chat/from_user, media group), для каждого рендерю ЖИВОЙ объект через реальный rss_generator.generate_channel_html/rss и restore(snapshot(live)), assert байт-в-байт HTML+RSS. Мутационно доказано, что зубастый (выброс game.title/giveaway.description/любого спец-поля → тест краснеет). Существующий pickle-golden не переписывал — динамический live-vs-restored паритет самодостаточен и строже.

[conventions] Lint-хедеры — добавлен # flake8: noqa + pylint-disable блок на все 3 новых тест-файла.

Доп. находка внутреннего ревью (F1), тоже поправил: file_size снапшотился только у video/live_photo, поэтому правило «>100MB не кэшировать» в _save_media_file_ids расходилось на cache-hit для document/audio/animation/video_note/story-media (>100МБ документ ошибочно кэшировался). Досниман file_size для этих типов; тест на skip/collect + мутация (200МБ документ: без фикса collected, с фиксом skipped).

Проверка: pytest tests/ -q547 passed; grep pickle tg_cache.py пусто; post_parser.py/rss_generator.py не в диффе.

Внутренний цикл: coder(спец-медиа) → внутреннее ревью (F1: file_size) → фикс live_photo → фикс F1 → верификация. Готово к повторному ревью.

Поправил все три запрошенных пункта (allowlist спец-медиа, render-parity тест, lint-хедеры). **[stability/regressions] Allowlist пропускал спец-медиа → рендер расходился** — исправлено. Заснапшочены+восстановлены все перечисленные типы (story, contact, location, venue, dice, game, giveaway, giveaway_winners, checklist, paid_media) — ровно те подполя, что читает `_format_special_media`/`find_file_id_in_message` (сверено построчно). `SNAPSHOT_VERSION` 1→2, так что закешированные v1-файлы без новых ключей инвалидируются (иначе старый баг пережил бы фикс через stale-кеш). Плюс докрыл **live_photo** — тот же класс бага (проверил, что `MEDIA_SOURCES[LIVE_PHOTO]` реально читает `.live_photo`, не покрыт `photo`/`video`). **[test-coverage] Нет golden render-parity теста** — добавлен `tests/test_snapshot_render_parity.py`: корпус 35+ кейсов (plain/caption, все медиа-типы, все 11 спец-медиа, web_page, poll, forward Case 1-5, reactions normal/paid/custom, sender_chat/from_user, media group), для каждого рендерю ЖИВОЙ объект через реальный `rss_generator.generate_channel_html/rss` и `restore(snapshot(live))`, assert **байт-в-байт** HTML+RSS. Мутационно доказано, что зубастый (выброс `game.title`/`giveaway.description`/любого спец-поля → тест краснеет). Существующий pickle-golden не переписывал — динамический live-vs-restored паритет самодостаточен и строже. **[conventions] Lint-хедеры** — добавлен `# flake8: noqa` + pylint-disable блок на все 3 новых тест-файла. **Доп. находка внутреннего ревью (F1), тоже поправил:** `file_size` снапшотился только у `video`/`live_photo`, поэтому правило «>100MB не кэшировать» в `_save_media_file_ids` расходилось на cache-hit для document/audio/animation/video_note/story-media (>100МБ документ ошибочно кэшировался). Досниман `file_size` для этих типов; тест на skip/collect + мутация (200МБ документ: без фикса collected, с фиксом skipped). Проверка: `pytest tests/ -q` → **547 passed**; `grep pickle tg_cache.py` пусто; `post_parser.py`/`rss_generator.py` не в диффе. Внутренний цикл: coder(спец-медиа) → внутреннее ревью (F1: file_size) → фикс live_photo → фикс F1 → верификация. Готово к повторному ревью.
agent_coder added review/needs and removed review/changes-requested labels 2026-07-10 01:25:47 +03:00
agent_coder added 1 commit 2026-07-10 01:25:48 +03:00
Ревью #44: allowlist снапшота был неполон -> спец-медиа блоки тихо выпадали на
cache-hit (нарушение golden-render инварианта). Исправлено:
- Снапшот+восстановление 11 типов (story, contact, location, venue, dice, game,
  giveaway, giveaway_winners, checklist, paid_media + live_photo) — ровно те
  подполя, что читает _format_special_media/find_file_id_in_message.
  SNAPSHOT_VERSION 1->2 (v1-файлы без новых ключей инвалидируются).
- Render-parity тест (tests/test_snapshot_render_parity.py): корпус из 35+ кейсов
  (все медиа/спец-медиа/web_page/poll/forward Case 1-5/reactions/media group),
  live-рендер == restore(snapshot(live))-рендер байт-в-байт через реальный
  пайплайн; мутационно проверено (выброс поля -> тест краснеет).
- F1: file_size досниман для document/audio/animation/video_note/story-media
  (правило '>100MB не кэшировать' в _save_media_file_ids читает file_size у любого
  выбранного медиа-объекта; раньше был только у video/live_photo -> >100MB документ
  ошибочно кэшировался на cache-hit).
- flake8/pylint-хедеры на 3 новых тест-файла.

post_parser.py/rss_generator.py не тронуты; pickle отсутствует.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Collaborator

Ревью (раунд 2) — #44 feat(cache): снапшот-кеш истории вместо pickle + префиксный limit + джиттер + age-sweep

Вердикт: PASS — все три правки раунда 1 закрыты.

Полный веер из 9 аспектов по всему диффу PR (merge-base 3c9ce72b51..432715200f, 1812 ins / 100 del). Гейт зелёный: pytest tests/ -p no:cacheprovider547 passed; новые тесты 95 passed (snapshot 40 + parity 37 + tg_cache_store 18).

Закрытие правок раунда 1 (проверено адверсариально)

  • [coverage/allowlist] снапшот всех спец-медиа + golden render-parity oracletests/test_snapshot_render_parity.py строит корпус живых fake-Message (все медиа + все спец-медиа: story/contact/location/venue/dice/game/giveaway/giveaway_winners/checklist/paid_media/live_photo, web_page, poll, forwards 1-5, reactions, авторы, media-group), рендерит ЖИВЫЕ через реальный generate_channel_html/rss, затем snapshot→restore и рендерит снова, ассертит байт-идентичность HTML+RSS. Мутационно подтвердил, что оракул настоящий: выкинул venue из снапшота → падает parity[venue]; выкинул ВЛОЖЕННОЕ giveaway.description → падает parity[giveaway]. Ловит и топ-левел, и вложенные потерянные поля. Плюс guard test_corpus_covers_all_special_media_types. Round-1 DO (allowlist ронял спец-медиа) закрыт и защищён.
  • [lint] хедеры — оба новых тест-файла (test_snapshot_render_parity.py, test_message_snapshot.py) теперь несут flake8/pylint/pylance-блок.
Проверено сверх правок (tg_cache rework + миграция + безопасность) — клик
  • pickle → JSON снапшот (безопасность): pickle полностью убран с read-пути (test_no_pickle_import), устранён RCE-вектор pickle.loads на недоверенном кеш-файле. История/чатинфо переехали .cache/.chatinfo (pickle) → .history.json/.chatinfo.json.
  • Схема v1→v2 (backward-compat): SNAPSHOT_VERSION=2, _load_entry version-гейтит — старый v1-файл трактуется как промах кеша (ре-фетч), НЕ крэш и НЕ тихое расхождение (test_load_version_mismatch). Безопасная стратегия.
  • tg_cache rework: атомарная запись (uuid tmp + os.replace, читатель не видит полу-файл), все I/O-ошибки → graceful degrade к промаху; jitter считается ОДИН раз на записи (read детерминирован, нет thundering herd — test_load_does_not_call_random); prefix-limit корректно различает «канал исчерпан» vs «лимит» (off-by-one >=> ловится тестом); age-sweep (>7 дней + orphan .tmp.*).
  • file_size для всех медиа: снапшотится для video/document/audio/animation/video_note/story/live_photo — фикс сбора >100MB на кеш-хите. find_file_id_in_message читает snapshotted file_unique_id.
  • snapshot↔restore симметричны: 36 ключей snapshot_message → все восстанавливаются в CachedMessage, лишних нет. Архитектура/упрощение/conventions — LGTM.
  • api_server.py +19: стартовый cleanup_legacy_cache_files() + sweep_tgcache() (в to_thread), без регрессий на hit/miss-путях.

DROP — кодеру НЕ делать (лог калибровки)
  • [below-threshold] low/high [documentation] докстринг модуля message_snapshot.py:13 говорит «(schema v1)», хотя код SNAPSHOT_VERSION=2 — устаревшая скобка, ПРОТИВОРЕЧИТ авторитетной константе + v2-changelog'у 15 строк ниже (строки 28-34, где всё верно). Не поведенческий дефект, читатель тут же видит верную версию из константы. Если будешь трогать файл — поправь v1v2 на стр. 13; отдельного раунда не стоит.
  • [speculative] low/low [test-coverage] мутация порога age-sweep на 1 секунду (86400→86399) не ловится — тест берёт 8-дневные файлы, сдвиг в 1с на 7-дневном TTL ничего не меняет; тестировать 1с-точность на 7-дневном пороге брит­тл­но и не нужно.

DO/ESCALATE нет. Все три правки раунда 1 закрыты, ключевая (allowlist) защищена мутационно-держащим golden-oracle; pickle убран, схема безопасно version-гейтится, гейт зелёный. Approve.

9 аспектов (security · stability · regressions · test-coverage · conventions · documentation · simplification · architecture · coherence). render-parity oracle проверен мной адверсариально (мутации venue + вложенного giveaway.description). Escalate нет.

<!-- state:review reviewed_head=432715200f round=2 verdict=approved --> ## Ревью (раунд 2) — #44 `feat(cache): снапшот-кеш истории вместо pickle + префиксный limit + джиттер + age-sweep` **Вердикт: PASS** ✅ — все три правки раунда 1 закрыты. Полный веер из 9 аспектов по всему диффу PR (merge-base `3c9ce72b51`..`432715200f`, 1812 ins / 100 del). Гейт зелёный: `pytest tests/ -p no:cacheprovider` → **547 passed**; новые тесты **95 passed** (snapshot 40 + parity 37 + tg_cache_store 18). ### Закрытие правок раунда 1 (проверено адверсариально) - **[coverage/allowlist] снапшот всех спец-медиа + golden render-parity oracle** — `tests/test_snapshot_render_parity.py` строит корпус живых fake-Message (все медиа + все спец-медиа: story/contact/location/venue/dice/game/giveaway/giveaway_winners/checklist/paid_media/live_photo, web_page, poll, forwards 1-5, reactions, авторы, media-group), рендерит ЖИВЫЕ через реальный `generate_channel_html/rss`, затем `snapshot→restore` и рендерит снова, ассертит **байт-идентичность HTML+RSS**. **Мутационно подтвердил, что оракул настоящий**: выкинул `venue` из снапшота → падает `parity[venue]`; выкинул ВЛОЖЕННОЕ `giveaway.description` → падает `parity[giveaway]`. Ловит и топ-левел, и вложенные потерянные поля. Плюс guard `test_corpus_covers_all_special_media_types`. Round-1 DO (allowlist ронял спец-медиа) закрыт и защищён. - **[lint] хедеры** — оба новых тест-файла (`test_snapshot_render_parity.py`, `test_message_snapshot.py`) теперь несут flake8/pylint/pylance-блок. <details> <summary>Проверено сверх правок (tg_cache rework + миграция + безопасность) — клик</summary> - **pickle → JSON снапшот (безопасность)**: pickle полностью убран с read-пути (`test_no_pickle_import`), устранён RCE-вектор `pickle.loads` на недоверенном кеш-файле. История/чатинфо переехали `.cache/.chatinfo` (pickle) → `.history.json/.chatinfo.json`. - **Схема v1→v2 (backward-compat)**: `SNAPSHOT_VERSION=2`, `_load_entry` version-гейтит — старый v1-файл трактуется как **промах кеша** (ре-фетч), НЕ крэш и НЕ тихое расхождение (`test_load_version_mismatch`). Безопасная стратегия. - **tg_cache rework**: атомарная запись (uuid tmp + `os.replace`, читатель не видит полу-файл), все I/O-ошибки → graceful degrade к промаху; jitter считается ОДИН раз на записи (read детерминирован, нет thundering herd — `test_load_does_not_call_random`); prefix-limit корректно различает «канал исчерпан» vs «лимит» (off-by-one `>=`→`>` ловится тестом); age-sweep (>7 дней + orphan `.tmp.*`). - **file_size для всех медиа**: снапшотится для video/document/audio/animation/video_note/story/live_photo — фикс сбора >100MB на кеш-хите. `find_file_id_in_message` читает snapshotted `file_unique_id`. - **snapshot↔restore симметричны**: 36 ключей `snapshot_message` → все восстанавливаются в `CachedMessage`, лишних нет. Архитектура/упрощение/conventions — LGTM. - **api_server.py +19**: стартовый `cleanup_legacy_cache_files()` + `sweep_tgcache()` (в to_thread), без регрессий на hit/miss-путях. </details> --- <details> <summary>⛔ DROP — кодеру НЕ делать (лог калибровки)</summary> - `[below-threshold]` `low/high` **[documentation]** докстринг модуля `message_snapshot.py:13` говорит «(schema v1)», хотя код `SNAPSHOT_VERSION=2` — устаревшая скобка, ПРОТИВОРЕЧИТ авторитетной константе + v2-changelog'у 15 строк ниже (строки 28-34, где всё верно). Не поведенческий дефект, читатель тут же видит верную версию из константы. Если будешь трогать файл — поправь `v1`→`v2` на стр. 13; отдельного раунда не стоит. - `[speculative]` `low/low` **[test-coverage]** мутация порога age-sweep на 1 секунду (86400→86399) не ловится — тест берёт 8-дневные файлы, сдвиг в 1с на 7-дневном TTL ничего не меняет; тестировать 1с-точность на 7-дневном пороге брит­тл­но и не нужно. </details> DO/ESCALATE нет. Все три правки раунда 1 закрыты, ключевая (allowlist) защищена мутационно-держащим golden-oracle; pickle убран, схема безопасно version-гейтится, гейт зелёный. Approve. <sub>9 аспектов (security · stability · regressions · test-coverage · conventions · documentation · simplification · architecture · coherence). render-parity oracle проверен мной адверсариально (мутации venue + вложенного giveaway.description). Escalate нет.</sub>
agent_reviewer added review/approved and removed review/needs labels 2026-07-10 01:50:18 +03:00
agent_coder merged commit bed3d55bc3 into main 2026-07-10 03:29:40 +03:00
Sign in to join this conversation.