refactor(render): этап 1 — sanitizer.py (#28, переоткрыт на main после стек-орфана) #37

Merged
vvzvlad merged 2 commits from refactor/render-stage1-sanitizer into main 2026-07-06 17:25:48 +03:00
Collaborator

Переоткрываю этап 1 (sanitizer) на main — при мёрже #36 стекнутый PR ушёл в ветку этапа-0, а не в main (тот же стек-орфан, что на #381). Проверил: в main НЕ попал, только оракул этапа 0.

Ребейзнул ветку этапа 1 на актуальный main (коммиты этапа 0 уже там, отвалились) — остался ровно дельта этапа 1 (sanitizer.py + per-post санитайз + §3.1–3.5/3.16 + golden по §3 + тест изоляции §3.4). База теперь main. Тот же заапрувленный на #36 код, полный сюит зелёный.

Влей этот PR — и этап 1 будет в main. Дальше этап 2 подниму с чистого main.

Переоткрываю этап 1 (sanitizer) на **main** — при мёрже #36 стекнутый PR ушёл в ветку этапа-0, а не в main (тот же стек-орфан, что на #381). Проверил: в main НЕ попал, только оракул этапа 0. Ребейзнул ветку этапа 1 на актуальный main (коммиты этапа 0 уже там, отвалились) — остался ровно дельта этапа 1 (sanitizer.py + per-post санитайз + §3.1–3.5/3.16 + golden по §3 + тест изоляции §3.4). База теперь **main**. Тот же заапрувленный на #36 код, полный сюит зелёный. Влей этот PR — и этап 1 будет в main. Дальше этап 2 подниму с чистого main.
agent_coder added 2 commits 2026-07-06 17:06:52 +03:00
Часть эпика рефакторинга рендер-пайплайна (#34). Один bleach-конфиг на весь
проект, один санитайз на пост, ноль thread-hop'ов в циклах (раньше конфиг был
скопирован трижды и разъехался; RSS-путь делал to_thread + новый CSSSanitizer +
вложенную функцию НА КАЖДЫЙ пост, хотя _render_pipeline уже в worker-треде).

Новый sanitizer.py — единственная bleach-конфигурация: ALLOWED_TAGS (union трёх
старых копий, вкл. s/del), ALLOWED_PROTOCOLS=['http','https','tg'],
sanitize_html() зовёт bleach.clean(..., protocols=..., strip=True) (оба не-дефолт),
fail-CLOSED (html.escape при любом исключении), единое имя лога
html_sanitization_error + log_context. _render_pipeline санитайзит каждый пост
после рендера (в своём worker-треде); rss_generator −71 строка.

Реестр §3:
- §3.1 s/del теперь в фидах (раньше выживали только в одиночном посте);
- §3.2 fail-open → fail-closed (вкл. single-post/JSON путь);
- §3.3 <hr class="post-divider"> реально виден (join ПОСЛЕ санитайза; hr не в
  whitelist, поэтому раньше вырезался strip=True);
- §3.4 несбалансированный фрагмент нормализуется в границах своего поста;
- §3.5 гранулярный fail-closed HTML-фида (падает один пост, не весь фид);
- §3.16 три имени санитайз-логов объединены в html_sanitization_error+log_context.

Golden-эталоны обновлены: КАЖДЫЙ изменённый байт привязан к пункту §3 (net-diff
тегов: только +<s>/§3.1 и +<hr>/§3.3; <div> сбалансирован +/- = §3.4;
после снятия этих токенов + нормализатора все 6 голденов побайтово old==new).
Оракул этапа 0 зелёный. Полный сюит 346 passed (+14 sanitizer-тестов).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Do 1 [test-coverage]: залочил security-инвариант §3.4 (per-post изоляция
фрагмента) именованным тестом test_unbalanced_post_does_not_swallow_next_post_html_feed
(в test_stage4_eventloop.py — гоняет реальный per-post HTML-пайплайн на истории из
2 сообщений). Пост A несёт висячий `<div><b>DANGLING_A` без закрывашек, пост B
целый с маркером. Ассертит: фид бьётся на 2 фрагмента по `<hr post-divider>`;
теги A сбалансированы В ПРЕДЕЛАХ фрагмента A; маркер B в фрагменте B и НЕ в A
(не заглочен). Проверено, что тест краснеет на регрессии «join-before-sanitize»:
при склейке raw-постов до единого whole-feed bleach.clean невайтлистнутый `<hr>`
стрипается (strip=True) → 0 разделителей → 1 фрагмент → первый ассерт падает.
То есть будущий этап, вернувший склейку до санитайза, ловится этим тестом.

Do 2 [documentation]: 4 устаревших коммента про удалённый «whole-feed sanitize
для HTML» обновлены на «per-post в _render_pipeline для обоих (RSS и HTML)»
(post_parser.py process_message docstring / _generate_html_body NOTE /
generate_html_footer coverage-map; rss_generator.py feed-path коммент). Вывод
«санитайзится ровно один раз» сохранён. Исполняемый код не менялся.

Полный сюит 347 passed (+1 тест изоляции); оракул этапа 0 — 11 passed, golden не менялся.

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

🤖 Авто-ревью · #37 (refactor(render): этап 1 — sanitizer.py, #28, переоткрыт на main), голова c2d9adc4

Вердикт: PASS (review/approved). Это ЗААПРУВЛЕННЫЙ контент #36 (обе итерации), переоткрытый на main — при мёрже #36 стекнутый PR ушёл в ветку этапа-0, а не в main (тот же стек-орфан, что на #381→#389). Контент я уже полностью отревьюил в #36 (round 1 CHANGES → round 2 PASS).

Проверено (ре-таргет одобренного контента)

  • Дерево БАЙТ-В-БАЙТ = одобренный #36 round-2 (711cb2cd): git diff 711cb2cd31 c2d9adc45fпусто. Другой SHA (ребейз на main), идентичное дерево (коммиты этапа-0 уже в main, отвалились из ребейза). Ноль конфликт-маркеров.
  • Всё содержимое одобренного round-2 на месте: sanitizer.py (единый конфиг, fail-closed), per-post санитайз в _render_pipeline, §3.1–3.5/3.16, golden по §3, §3.4 fragment-isolation тест, актуализированные комменты.
  • Гейт зелёный на main-базе: полный pytest tests/347 passed.

Контент идентичен ранее одобренному #36 round-2 (там веер 9 аспектов: security fail-closed + безопасный superset + нет cross-post-bleed; golden-аудит — каждое изменение §3-объяснено, ноль потери контента; §3.4 чинит латентный баг; stability thread-safe). Мержибельно — это и закрывает этап 1/6 эпика #34 на main.

Ре-таргет одобренного PR на main после стек-орфана; дерево диффом подтверждено идентичным #36 round-2, гейт 347 passed на новой базе.

## 🤖 Авто-ревью · #37 (refactor(render): этап 1 — sanitizer.py, #28, переоткрыт на main), голова `c2d9adc4` **Вердикт: PASS** ✅ (review/approved). Это ЗААПРУВЛЕННЫЙ контент #36 (обе итерации), переоткрытый на `main` — при мёрже #36 стекнутый PR ушёл в ветку этапа-0, а не в main (тот же стек-орфан, что на #381→#389). Контент я уже полностью отревьюил в #36 (round 1 CHANGES → round 2 PASS). ### Проверено (ре-таргет одобренного контента) - **Дерево БАЙТ-В-БАЙТ = одобренный #36 round-2** (`711cb2cd`): `git diff 711cb2cd31 c2d9adc45f` — **пусто**. Другой SHA (ребейз на main), идентичное дерево (коммиты этапа-0 уже в main, отвалились из ребейза). Ноль конфликт-маркеров. - Всё содержимое одобренного round-2 на месте: `sanitizer.py` (единый конфиг, fail-closed), per-post санитайз в `_render_pipeline`, §3.1–3.5/3.16, golden по §3, §3.4 fragment-isolation тест, актуализированные комменты. - **Гейт зелёный на main-базе:** полный `pytest tests/` — **347 passed**. Контент идентичен ранее одобренному #36 round-2 (там веер 9 аспектов: security fail-closed + безопасный superset + нет cross-post-bleed; golden-аудит — каждое изменение §3-объяснено, ноль потери контента; §3.4 чинит латентный баг; stability thread-safe). Мержибельно — это и закрывает этап 1/6 эпика #34 на main. <sub>Ре-таргет одобренного PR на main после стек-орфана; дерево диффом подтверждено идентичным #36 round-2, гейт 347 passed на новой базе.</sub> <!-- state:review reviewed_head=c2d9adc45f round=retarget verdict=pass -->
agent_reviewer added review/approved and removed review/needs labels 2026-07-06 17:09:22 +03:00
vvzvlad merged commit 53da67eacf into main 2026-07-06 17:25:48 +03:00
Sign in to join this conversation.