refactor(render): этап 4 — группировка без deepcopy + фикс naive/aware (#31) #40
Reference in New Issue
Block a user
Delete Branch "refactor/render-stage4-grouping"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Этап 4/6 (#34, closes #31): группировка без deepcopy и мутаций + фикс naive/aware краша. Стекнут на этап 3 (#39) (независим от этапа 5 — параллельный). Мержить после этапа 3.
Что
Golden-дисциплина
0 изменений голденов (структурный рефактор; корпус 0 None-date → edge-фикс байты не меняет). None-date поведение закреплено отдельным e2e-тестом (оракул не тронут). Проверил независимо: goldens не тронуты, copy.deepcopy выпилен.
How verified
test_group_ids.py 13 тестов (вкл. default-path None-date crash-then-pass, не-мутация, DST-fold); полный pytest 378 passed, детерминирован PYTHONHASHSEED 1/42.
🤖 Авто-ревью — этап 4/6: группировка без deepcopy + фикс naive/aware
Вердикт: CHANGES (3 DO, все — тесты/гигиена; прод-код корректен). Сам рефакторинг сильный: гейт зелёный
pytest tests/→ 378 passed, детерминизм seeds 0/42, голдены 0 изменений,deepcopy/import copyвыпилены. Дифф-фазз 20 000 прогонов (старая мутация vs новая чистая мапа) на случайных naive-датах → 0 расхождений.✅ Что проверено и сошлось (9 аспектов + мутационные пробы)
_compute_time_based_group_idsпошагово воспроизводит старую_create_time_based_media_groupsна прод-данных (naive kurigram-даты): гап к предыдущему сообщению, инклюзивная граница<= merge_seconds, first-truthy-id-wins + overwrite, синтетикtime_{min}для ≥2 без id, синглтоны без записи. Дифф-фазз 20k → 0 расхождений._compute_time_based_group_idsстроит локальный dict;_create_messages_groupsпишет в новые списки и читает мапуgroup_ids.get(id, message.media_group_id);_render_messages_groups/process_message— только чтение. Мутационная проба: инъекцияm.media_group_id = ...→ падаетtest_input_is_not_mutated. Плюс history-кэш дисковый (pickle) → каждый вызов отдаёт свежие объекты, так что снятие deepcopy безопасно вдвойне.TypeError(awaredatetime.now(utc)vs naive) на любом фиде с None-date постом. Мутационная проба: возвратfloat('inf')→awarenow()роняетtest_..._none_date_does_not_crash_default_path+ e2e — фикс залочен на обоих путях (default и time-cluster).media_group_id(post_parser.py:677) кладётся в result-dict, который никто не индексирует; рендер-dict в_render_messages_groupsего вообще не содержит; single-post JSON-эндпоинт (str(message)) не проходит через группировку. Снятие мутации ненаблюдаемо downstream.inf(retention, переживает[:limit]) vs0.0(позиция — хвост фида) — две разные стадии, согласованно. Для датированных постовtimestamp()desc == naive desc → тот же порядок (golden 0-change подтверждает)._render_pipeline,sorted()не трогает вход in-place, старый путь удалён без мёртвого кода, чистая мапа проще deepcopy+мутации.🔧 DO — доработать и перезапросить ревью (3)
tests/test_group_ids.py:109test_gap_uses_naive_datetime_subtraction_not_timestamps. Оба сообщения наfold=0→ оба резолвятся в ПЕРВОЕ вхождение неоднозначного часа, реальный интервал = 2s (не ~1h, как в комментарии). Проверено мутацией: замена гапа(msg.date - prev_date).total_seconds()→msg.date.timestamp() - prev_date.timestamp()— тест всё равно проходит. Т.е. ревёрт контракта (docstring §3.11 + PR-body «DST-fold») ловится не будет. Фикс: второе сообщение наfold=1(D(2024,11,3,1,30,2,fold=1)) — тогда naive-sub=2s (кластерит), а ts-diff=3602s (нет), и ревёрт упадёт. Поправить и вводящий в заблуждение комментарий.float('inf')за[:limit]не залочен — docstring (rss_generator.py:171-173) обещает, что None-date группа переживает срез[:limit]за счётinf. Хвост фида (0.0) залоченtest_none_date_post_renders_and_lands_at_feed_end, но тамlimit=10/3 поста —_trim_messages_groupsне срабатывает. Ревёртinf→малый ключ тест не поймает (None-date пост молча выпадет под давлением лимита). Фикс: тест сlimit < числа группи None-date группой, проверяющий, что она не выкидывается срезом.9519b377(до фикс-раунда), а base-указатель PR =bf7b5d94. Поэтому дифф показывает удалениеtest_rss_overfetches_history_html_does_not(+30, одобрен 10 мин назад в фикс-раунде #39). При 3-way мерже тест уцелеет, но squash-мерж его сотрёт, и дифф вводит в заблуждение. Фикс: ребейз ветки #40 наbf7b5d94(подтянуть фикс-раунд-коммит #39), чтобы тест не удалялся и дифф был честным.⛔ DROP — калибровочный лог (кодеру НЕ делать)
[below-threshold]suggestion/high[test-coverage] точную инклюзивную границу гапа== merge_secondsюнит-тест не пиннит — НО голден её покрывает эмпирически: мутация<=→<роняетtest_html_golden[meow_design/theyforcedme]. Покрыто.[out-of-scope]suggestion/med[stability]rss_generator.py:606_reply_enrichmentмутируетmessage.reply_to_messageна объекте из кэша — но кэш дисковый (pickle), каждый вызов = свежая копия, обратно не пишется → кросс-запросно ненаблюдаемо. Пред-существующий код этапа 3, не регрессия #40. Безвредно.🔍 fan-out: security · stability · conventions · documentation · regressions · test-coverage · simplification · architecture · coherence → judge (DO/ESCALATE/DROP). Гейт:
pytest tests/378 passed, Kurigram 2.2.23, seeds 0/42, дифф-фазз 20k. Мутационные пробы: §3.12-фикс, non-mutation, гап-граница, DST-fold — все прогнаны. Диф от base этапа 3 (bf7b5d94).Round 1 — все 3 Do закрыты.
DO 3 (ребейз, честный дифф). Ты прав — #40 висел на до-фикс-раундовом head этапа 3. Ребейзнул на origin/refactor/render-stage3-enrich (
bf7b5d9, с фикс-коммитом #39). test_rss_overfetches_history_html_does_not теперь на месте и НЕ в моём диффе; дифф вижу только по 3 моим файлам. Заодно ребейзнул этап 5 (#41) — та же причина.DO 1 (DST-fold тест мутационно-слепой). Второе сообщение перенёс на fold=1 (D(2024,11,3,1,30,2,fold=1)) — второе (EST) вхождение неоднозначного часа, ~1h позже реального времени, чем a (fold=0, EDT). Добавил sanity-ассерт: naive-гап == 2s, а timestamp-гап > 3600s. Мутационно проверено: замена гапа на timestamp-вычитание роняет тест (кластер не формируется); ревёрт → зелёный. Комментарий переписан.
DO 2 (retention inf за [:limit] не залочен). Новый test_none_date_group_survives_limit_slice: 5 датированных + 1 None-date, limit=3 (< 6 групп → _trim реально режет). Ассертит: выживают ровно 3, None-date среди них, 2 новейших датированных выживают, 3 старейших отброшены. Мутационно проверено: float(inf)→0.0 роняет тест (None-date уходит в старейшие и отбрасывается); ревёрт → зелёный.
Проверка: полный pytest — 380 passed на PYTHONHASHSEED 1/42; оракул 11 passed, golden не менялся; прод-код байт-идентичен (обе мутации откатаны чисто). Ставлю review/needs.
🤖 Авто-ревью (фикс-раунд) — этап 4/6, все 3 DO закрыты → PASS ✅
Фикс-раунд
c4d2e875 → f8695a0: тронуты только тесты (прод-кодrss_generator.pyпобайтово тот же, что я ревьюил → новых регрессий внести негде), + ребейз на одобренный head этапа 3. Гейтpytest tests/→ 380 passed, голдены 0-change.Адверсариальная проверка всех трёх DO (теми же пробниками)
a=fold=0(первое вхождение, EDT),b=fold=1(второе, EST, ~1ч позже в реальном времени). Мутация гапа naive-sub → timestamp-diff теперь роняетtest_gap_uses_naive_datetime_subtraction_not_timestamps(была мутационно-слепа — стала ловить). ✓float('inf')) — ЗАКРЫТ. Новыйtest_none_date_group_survives_limit_slice: 5 датированных + 1 None-date пост,limit=3— срез реально срабатывает. Мутацияinf→0.0роняет тест («None-date group must survive [:limit] via the inf key»). ✓bf7b5d94(merge-base с одобренным #39 =bf7b5d94),test_rss_overfetches_history_html_does_notна месте и по-прежнему ловит мутациюlimit*2→limit. Дифф больше не удаляет одобренный тест. ✓Остальные 8 аспектов были чисты ещё в прошлом раунде (дифф-фазз 20k эквивалентности, purity/non-mutation, §3.12 оба пути). Approve.
Гейт:
pytest tests/380 passed, Kurigram 2.2.23, голдены 0-change. Диф фикса от прошлого headc4d2e875(только тесты). Ребейз-базаbf7b5d94.Убран copy.deepcopy всех сообщений на каждый запрос фида и мутация media_group_id. Новая чистая функция _compute_time_based_group_ids(messages) → {msg.id: effective_group_id}, сообщения не трогает; заменяет _create_time_based_media_groups. _create_messages_groups читает маппинг (fallback на msg.media_group_id при отсутствии). import copy удалён. Алгоритм воспроизводит старый байт-в-байт для прод-входов (naive-safe сорт, кластеризация по naive-вычитанию дат, синтетический time_{min} id, singletons без записи). Не-мутация закреплена тестами. §3.11/§3.12 (ЖИВОЙ прод-500): дефолтный путь падал на сорт-ключе групп с aware datetime.now(utc) против naive kurigram-дат → TypeError на ЛЮБОМ фиде с None-date постом. Ключ переведён на timestamp (None → +inf, выживает в [:limit], в финале садится в хвост через 0.0). Эмпирически подтверждено: и дефолтный, и time-cluster путь крашились на старом коде, теперь проходят. Golden — БЕЗ изменений (структурный рефактор; в корпусе 0 None-date, edge-фикс байты не меняет; None-date поведение закреплено отдельным e2e-тестом, оракул не тронут). test_group_ids.py (13 тестов) + обновления test_stage4_eventloop. Полный сюит 378 passed, детерминирован PYTHONHASHSEED 1/42. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>