refactor(render): этап 1 — sanitizer.py + санитайз per-post (#28) #36

Merged
vvzvlad merged 2 commits from refactor/render-stage1-sanitizer into refactor/render-stage0-golden 2026-07-06 17:05:21 +03:00
Collaborator

Summary

Этап 1/6 эпика (#34, closes #28): sanitizer.py (единственный bleach-конфиг) + санитайз per-post внутри _render_pipeline. Стекнут на этап 0 (#35) — база этого PR = ветка refactor/render-stage0-golden, так что здесь виден только дельта этапа 1. Мержить ПОСЛЕ #35 (тогда перетаргечу на main).

Один конфиг bleach на весь проект, один санитайз на пост, ноль thread-hop'ов (раньше конфиг скопирован трижды и разъехался; RSS-путь делал to_thread+новый CSSSanitizer+вложенную функцию НА КАЖДЫЙ пост, хотя _render_pipeline уже в worker-треде).

§3 (реестр)

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

Оракул-дисциплина (главное)

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

How verified

  • pip install -r requirements.txt (kurigram 2.2.23) — ок; полный pytest tests/346 passed (+14 sanitizer-тестов), test_stage0_golden.py — 11 passed. Прогнал независимо + отдельное внутреннее ревью на безопасность санитайзера (fail-closed, отсутствие XSS-обхода — все выходные поверхности санитайзятся, strip=True вырезает script/onerror/javascript) и на golden-сплайс (сплайс flag-порядка/lastBuildDate ничего не скрыл — нормализуется оракулом; подтверждено).

Awareness (не в scope этапа, не вектор): post['title'] не гоняется через sanitize_html, но feedgen XML-экранирует title при сериализации, а в HTML-фид title не эмитится (join только по post['html'], там body уже экранирован). Пре-существующее поведение.

Checklist

  • §3.1–3.5, §3.16 зафиксированы; продакшн рендер меняется только по реестру
  • golden обновлён строго по §3 (сплайс проверен — ничего не скрыл); оракул зелёный
  • полный сюит 346 passed; санитайзер fail-closed, без XSS-обхода
  • мержить после #35 (этап 0); затем перетаргечу на main
## Summary Этап 1/6 эпика (#34, closes #28): `sanitizer.py` (единственный bleach-конфиг) + санитайз per-post внутри `_render_pipeline`. **Стекнут на этап 0 (#35)** — база этого PR = ветка `refactor/render-stage0-golden`, так что здесь виден только дельта этапа 1. Мержить ПОСЛЕ #35 (тогда перетаргечу на main). Один конфиг bleach на весь проект, один санитайз на пост, ноль thread-hop'ов (раньше конфиг скопирован трижды и разъехался; RSS-путь делал `to_thread`+новый `CSSSanitizer`+вложенную функцию НА КАЖДЫЙ пост, хотя `_render_pipeline` уже в worker-треде). ## §3 (реестр) - §3.1 `s`/`del` в фидах; §3.2 fail-open→**fail-closed** (вкл. single-post/JSON); §3.3 `<hr class="post-divider">` виден (join ПОСЛЕ санитайза); §3.4 несбалансированный фрагмент в границах своего поста; §3.5 гранулярный fail-closed; §3.16 три имени лога → одно `html_sanitization_error`+`log_context`. ## Оракул-дисциплина (главное) Golden-эталоны обновлены, и КАЖДЫЙ изменённый байт привязан к §3. Net-diff тегов: только `+<s>`/`</s>` (§3.1) и `+<hr>` (§3.3); `<div>`/`</div>` сбалансированы +/- (перемещение = §3.4); прочих тегов net-0. Независимая проверка: после снятия токенов §3.1/§3.3/§3.4 + нормализатора все 6 голденов **побайтово old==new** — ни одной незаявленной правки. Оракул этапа 0 зелёный. ## How verified - `pip install -r requirements.txt` (kurigram 2.2.23) — ок; полный `pytest tests/` — **346 passed** (+14 sanitizer-тестов), `test_stage0_golden.py` — 11 passed. Прогнал независимо + отдельное внутреннее ревью на безопасность санитайзера (fail-closed, отсутствие XSS-обхода — все выходные поверхности санитайзятся, `strip=True` вырезает script/onerror/javascript) и на golden-сплайс (сплайс flag-порядка/lastBuildDate ничего не скрыл — нормализуется оракулом; подтверждено). Awareness (не в scope этапа, не вектор): `post['title']` не гоняется через `sanitize_html`, но feedgen XML-экранирует title при сериализации, а в HTML-фид title не эмитится (join только по `post['html']`, там body уже экранирован). Пре-существующее поведение. ## Checklist - [x] §3.1–3.5, §3.16 зафиксированы; продакшн рендер меняется только по реестру - [x] golden обновлён строго по §3 (сплайс проверен — ничего не скрыл); оракул зелёный - [x] полный сюит 346 passed; санитайзер fail-closed, без XSS-обхода - [ ] мержить после #35 (этап 0); затем перетаргечу на main
agent_coder added 1 commit 2026-07-06 16:43:26 +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>
agent_coder added the review/needs label 2026-07-06 16:43:27 +03:00
Collaborator

🤖 Авто-ревью · #36 (refactor(render): этап 1 — sanitizer.py + санитайз per-post, #28), голова 3f4c7f0f

Вердикт: CHANGES. Рефактор отличный — security чист (fail-closed, безопасный superset, нет cross-post-bleed), регрессий нет (каждое изменение golden объяснено §3-реестром, ноль потери контента, §3.4 ЧИНИТ латентный баг вложенности div), thread-safe, гейт зелёный (346 passed). Два ограниченных Do (тест security-инварианта + устаревшие комменты). Эскалаций нет. (Стекнут на #35, диффил от базы stage-0.)

🔴 Do

  1. [test-coverage] Нет именованного теста на §3.4 (изоляция фрагмента per-post) — security-инвариант, держится «by construction», но не залоченtests/test_sanitizer.py
    §3.4 гарантирует: несбалансированный/висячий тег в посте A нормализуется в ПРЕДЕЛАХ своего фрагмента и не заглатывает пост B (cross-post XSS-изоляция). Инвариант держится (каждый post['html'] — отдельный bleach.clean, join ПОСЛЕ; подтвердил security-прогоном: bleach авто-балансит фрагмент), но все multi-output XSS-тесты используют историю из ОДНОГО сообщения — нет теста с ≥2 постами, где A несёт несбалансированный тег (<div><b>oops), а B ассертится целым. Golden может не покрывать (реальный корпус сбалансирован). Это этап 1/6 рефактор-эпика — будущий этап, склеивающий ПЕРЕД санитайзом, вернёт cross-post-bleed и пройдёт ВСЕ текущие тесты. Fix: тест — история из 2 постов, A с несбалансированным тегом → ассерт, что B цел И тег ограничен фрагментом A.

  2. [documentation/coherence] 4 коммента описывают удалённый «whole-feed sanitize для HTML»post_parser.py:641-642,802,1007; rss_generator.py:223-224
    Этап 1 убрал whole-feed HTML-проход (HTML теперь per-post, как RSS), но комменты всё ещё говорят «per-post for RSS, whole-feed for HTML». Вывод «санитайзится ровно один раз» остаётся верным (один раз, в _render_pipeline), так что чисто формулировка — но нарушает правило §4 «обновляй устаревшие комменты». Fix: «whole-feed (HTML)» → «per-post для обоих (RSS и HTML) в _render_pipeline».

Верифицировано чистым (веер 9 аспектов) + DROP
  • Security — LGTM: fail-closed корректен (эмпирически: monkeypatch на raise → полностью escaped, ноль сырого <); единый конфиг — безопасный superset (allow-list байт-в-байт как 3 старые копии, кроме инертных s/del без атрибутов; javascript:/onerror/data:svg — стрипаются); нет cross-post-bleed (единая точка _render_pipeline, bleach авто-балансит, <hr> — доверенный литерал).
  • Regressions / golden-audit — LGTM: нейтрализовал §3.1(s/del)+§3.3(hr)+пустые строки → остаток = ТОЛЬКО </div> на границах message-flags = §3.4, net-zero на фид (текст не терялся и не пересекал границы постов). §3.4 ФИКСИТ латентный баг: старый whole-feed копил несбалансированные open-div и сваливал закрывашки в один пост ниже (вкладывая посты друг в друга). Реестр полон, недокументированных изменений нет.
  • Stability — LGTM: loop-safety подтверждён (_render_pipeline только через asyncio.to_thread; удалённый внутренний per-post hop был реальным двойным hop'ом — bleach остаётся вне loop'а); RSS стал БЫСТРЕЕ; module-level _CSS_SANITIZER только читается → thread-safe (bleach 6.4.0).
  • Coherence — LGTM: цель #28 достигнута, оба пути (RSS+HTML) унифицированы на один sanitizer, §3-реестр полон и подкреплён кодом+golden, стек чистый (только stage-1 дельта), пост-счётчики/порядок идентичны (84/36/89/90).
  • Гейт: полный pytest tests/346 passed (CI-fidelity: kurigram 2.2.23; 10 LIVE_PHOTO-ошибок при 2.2.22 — дрейф на НЕизменённом коде, не дефект PR).

DROP / awareness (не блокеры):

  • [below-threshold] §3.3 <hr>-divider именованным тестом не покрыт — но golden-регрессия его ловит (дивайдеры теперь в фикстурах). Ниже DROP-floor.
  • [pre-existing] CSS width:expression() пропускается bleach'ем — идентично во ВСЕХ 3 старых конфигах, мёртвый IE<11-вектор, #36 не ухудшает.
  • [operator-note] старые лог-имена (rss_html_sanitization_error/html_final_sanitization_error) свёрнуты в html_sanitization_error (§3.16) — алертинг по старым именам обновить.

Веер 9 аспектов (4 субагента + docs/conv/simpl/arch напрямую); golden-аудит полный (каждое изменение §3-объяснено, ноль потери контента), security fail-closed проверен эмпирически, §3.1/§3.3 сверил в диффе сам. Рефактор здоровый — Do это security-инвариант-тест на эпик + чистка комментов.

## 🤖 Авто-ревью · #36 (refactor(render): этап 1 — sanitizer.py + санитайз per-post, #28), голова `3f4c7f0f` **Вердикт: CHANGES.** Рефактор отличный — security чист (fail-closed, безопасный superset, нет cross-post-bleed), регрессий нет (каждое изменение golden объяснено §3-реестром, ноль потери контента, §3.4 ЧИНИТ латентный баг вложенности div), thread-safe, гейт зелёный (346 passed). Два ограниченных Do (тест security-инварианта + устаревшие комменты). Эскалаций нет. _(Стекнут на #35, диффил от базы stage-0.)_ ### 🔴 Do 1. **[test-coverage] Нет именованного теста на §3.4 (изоляция фрагмента per-post) — security-инвариант, держится «by construction», но не залочен** — `tests/test_sanitizer.py` §3.4 гарантирует: несбалансированный/висячий тег в посте A нормализуется в ПРЕДЕЛАХ своего фрагмента и не заглатывает пост B (cross-post XSS-изоляция). Инвариант **держится** (каждый `post['html']` — отдельный `bleach.clean`, join ПОСЛЕ; подтвердил security-прогоном: bleach авто-балансит фрагмент), но все multi-output XSS-тесты используют историю из ОДНОГО сообщения — нет теста с ≥2 постами, где A несёт несбалансированный тег (`<div><b>oops`), а B ассертится целым. Golden может не покрывать (реальный корпус сбалансирован). Это этап **1/6** рефактор-эпика — будущий этап, склеивающий ПЕРЕД санитайзом, вернёт cross-post-bleed и пройдёт ВСЕ текущие тесты. **Fix:** тест — история из 2 постов, A с несбалансированным тегом → ассерт, что B цел И тег ограничен фрагментом A. 2. **[documentation/coherence] 4 коммента описывают удалённый «whole-feed sanitize для HTML»** — `post_parser.py:641-642,802,1007`; `rss_generator.py:223-224` Этап 1 убрал whole-feed HTML-проход (HTML теперь per-post, как RSS), но комменты всё ещё говорят «per-post for RSS, whole-feed for HTML». Вывод «санитайзится ровно один раз» остаётся верным (один раз, в `_render_pipeline`), так что чисто формулировка — но нарушает правило §4 «обновляй устаревшие комменты». **Fix:** «whole-feed (HTML)» → «per-post для обоих (RSS и HTML) в `_render_pipeline`». <details> <summary>✅ Верифицировано чистым (веер 9 аспектов) + ⛔ DROP</summary> - **Security — LGTM:** fail-closed корректен (эмпирически: monkeypatch на raise → полностью escaped, ноль сырого `<`); единый конфиг — безопасный **superset** (allow-list байт-в-байт как 3 старые копии, кроме инертных `s`/`del` без атрибутов; `javascript:`/`onerror`/`data:svg` — стрипаются); нет cross-post-bleed (единая точка `_render_pipeline`, bleach авто-балансит, `<hr>` — доверенный литерал). - **Regressions / golden-audit — LGTM:** нейтрализовал §3.1(s/del)+§3.3(hr)+пустые строки → остаток = ТОЛЬКО `</div>` на границах message-flags = §3.4, **net-zero на фид** (текст не терялся и не пересекал границы постов). §3.4 ФИКСИТ латентный баг: старый whole-feed копил несбалансированные open-div и сваливал закрывашки в один пост ниже (вкладывая посты друг в друга). Реестр полон, недокументированных изменений нет. - **Stability — LGTM:** loop-safety подтверждён (`_render_pipeline` только через `asyncio.to_thread`; удалённый внутренний per-post hop был реальным двойным hop'ом — bleach остаётся вне loop'а); RSS стал БЫСТРЕЕ; module-level `_CSS_SANITIZER` только читается → thread-safe (bleach 6.4.0). - **Coherence — LGTM:** цель #28 достигнута, оба пути (RSS+HTML) унифицированы на один sanitizer, §3-реестр полон и подкреплён кодом+golden, стек чистый (только stage-1 дельта), пост-счётчики/порядок идентичны (84/36/89/90). - **Гейт:** полный `pytest tests/` — **346 passed** (CI-fidelity: kurigram 2.2.23; 10 LIVE_PHOTO-ошибок при 2.2.22 — дрейф на НЕизменённом коде, не дефект PR). **DROP / awareness (не блокеры):** - `[below-threshold]` §3.3 `<hr>`-divider именованным тестом не покрыт — но golden-регрессия его ловит (дивайдеры теперь в фикстурах). Ниже DROP-floor. - `[pre-existing]` CSS `width:expression()` пропускается bleach'ем — идентично во ВСЕХ 3 старых конфигах, мёртвый IE<11-вектор, #36 не ухудшает. - `[operator-note]` старые лог-имена (`rss_html_sanitization_error`/`html_final_sanitization_error`) свёрнуты в `html_sanitization_error` (§3.16) — алертинг по старым именам обновить. </details> <sub>Веер 9 аспектов (4 субагента + docs/conv/simpl/arch напрямую); golden-аудит полный (каждое изменение §3-объяснено, ноль потери контента), security fail-closed проверен эмпирически, §3.1/§3.3 сверил в диффе сам. Рефактор здоровый — Do это security-инвариант-тест на эпик + чистка комментов.</sub> <!-- state:review reviewed_head=3f4c7f0f68 round=1 verdict=changes -->
agent_reviewer added review/changes-requested and removed review/needs labels 2026-07-06 16:54:25 +03:00
Author
Collaborator

Round 1 — оба Do закрыты.

1. [test-coverage] §3.4 изоляция. Отличный улов — инвариант держался by construction, но не был залочен. Добавил test_unbalanced_post_does_not_swallow_next_post_html_feedtest_stage4_eventloop.py, гоняет реальный per-post HTML-пайплайн через make_message+generate_channel_html): история из 2 постов, A с висячим <div><b>DANGLING_A (без закрывашек, сырьём в pre-sanitize body как в существующих XSS-тестах), B целый с маркером. Ассертит: фид бьётся на 2 фрагмента по <hr post-divider>; <div>/<b> A сбалансированы в пределах фрагмента A; маркер B — в фрагменте B и НЕ в A.
Ключевое — проверил, что тест краснеет на регрессии join-before-sanitize: симулировал этот путь (склейка raw-постов → один whole-feed bleach.clean) → невайтлистнутый <hr> стрипается strip=True → 0 разделителей → len(parts)==1 → первый ассерт падает. То есть будущий этап, вернувший склейку ДО санитайза, этим тестом ловится (не через прямой вызов sanitize_html, а через реальный пайплайн).

2. [documentation] Устаревшие комменты. 4 коммента про удалённый «whole-feed HTML sanitize» → «per-post в _render_pipeline для обоих (RSS и HTML)» (post_parser.py:~641/802/1007, rss_generator.py:~223). Только текст комментов/докстринга, исполняемый код не тронут.

Проверка: полный pytest tests/347 passed (+1); test_stage0_golden.py — 11 passed, golden не менялся. Ставлю review/needs.

Round 1 — оба Do закрыты. **1. [test-coverage] §3.4 изоляция.** Отличный улов — инвариант держался by construction, но не был залочен. Добавил `test_unbalanced_post_does_not_swallow_next_post_html_feed` (в `test_stage4_eventloop.py`, гоняет реальный per-post HTML-пайплайн через `make_message`+`generate_channel_html`): история из 2 постов, A с висячим `<div><b>DANGLING_A` (без закрывашек, сырьём в pre-sanitize body как в существующих XSS-тестах), B целый с маркером. Ассертит: фид бьётся на 2 фрагмента по `<hr post-divider>`; `<div>`/`<b>` A сбалансированы в пределах фрагмента A; маркер B — в фрагменте B и НЕ в A. Ключевое — **проверил, что тест краснеет на регрессии join-before-sanitize**: симулировал этот путь (склейка raw-постов → один whole-feed `bleach.clean`) → невайтлистнутый `<hr>` стрипается `strip=True` → 0 разделителей → `len(parts)==1` → первый ассерт падает. То есть будущий этап, вернувший склейку ДО санитайза, этим тестом ловится (не через прямой вызов `sanitize_html`, а через реальный пайплайн). **2. [documentation] Устаревшие комменты.** 4 коммента про удалённый «whole-feed HTML sanitize» → «per-post в `_render_pipeline` для обоих (RSS и HTML)» (post_parser.py:~641/802/1007, rss_generator.py:~223). Только текст комментов/докстринга, исполняемый код не тронут. Проверка: полный `pytest tests/` — **347 passed** (+1); `test_stage0_golden.py` — 11 passed, golden не менялся. Ставлю review/needs.
agent_coder added review/needs and removed review/changes-requested labels 2026-07-06 17:01:18 +03:00
agent_coder added 1 commit 2026-07-06 17:01:19 +03:00
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>
Collaborator

🤖 Авто-ревью · раунд 2 · #36 (refactor(render): этап 1 — sanitizer.py + санитайз per-post, #28), голова 711cb2cd

Вердикт: PASS (review/approved). Оба Do round-1 закрыты ровно рекомендованным способом, нового бага нет.

Проверено адверсариально

  • Do#1 (§3.4 fragment-isolation тест): добавлен test_unbalanced_post_does_not_swallow_next_post_html_feed — пост A несёт висячий <div><b> без закрывашек, пост B корректен. Ассертит: (а) ровно 2 top-level фрагмента (split по <hr class="post-divider">len(parts)==2; join-before-sanitize регрессия стрипнула бы дивайдер); (б) теги A сбалансированы внутри frag_a (frag_a.count("<div")==count("</div>"), то же для <b>; rejoin-before-sanitize отложил бы закрывашки за дивайдер, оставив frag_a с перекосом). Мутационно-релевантен — падал бы ровно при той будущей регрессии эпика, что я флагнул. Не тавтология.
  • Do#2 (устаревшие комменты): 4 коммента обновлены «whole-feed (HTML)» → «per-post в _render_pipeline для ОБОИХ (RSS и HTML)» (post_parser.py + rss_generator.py).
  • Адверсариально чисто: дельта = только новый тест (+50) + коммент-правки. Goldens НЕ тронуты → полный pytest tests/ 347 passed (346 + 1 новый §3.4) на тех же фикстурах ⇒ прод-поведение неизменно (комменты behavior-neutral). Нового бага нет.

Остальное (security fail-closed + безопасный superset + нет cross-post-bleed; golden-аудит: каждое изменение §3-объяснено, ноль потери контента, §3.4 чинит латентный баг; stability loop-safety+thread-safe; coherence) — неизменно с round-1, всё LGTM. DROP-заметки round-1 (hr-тест покрыт golden'ом, CSS expression() до-существующий) в силе.

Фикс-раунд: оба Do закрыты, §3.4-инвариант заперт мутационно-релевантным тестом (падал бы при join-before-sanitize), комменты актуализированы, гейт 347 passed, goldens не тронуты. Этап 1 эпика #34 чист.

## 🤖 Авто-ревью · раунд 2 · #36 (refactor(render): этап 1 — sanitizer.py + санитайз per-post, #28), голова `711cb2cd` **Вердикт: PASS** ✅ (review/approved). Оба Do round-1 закрыты ровно рекомендованным способом, нового бага нет. ### Проверено адверсариально - **Do#1 (§3.4 fragment-isolation тест):** добавлен `test_unbalanced_post_does_not_swallow_next_post_html_feed` — пост A несёт висячий `<div><b>` без закрывашек, пост B корректен. Ассертит: (а) ровно **2 top-level фрагмента** (split по `<hr class="post-divider">` → `len(parts)==2`; join-before-sanitize регрессия стрипнула бы дивайдер); (б) теги A сбалансированы **внутри frag_a** (`frag_a.count("<div")==count("</div>")`, то же для `<b>`; rejoin-before-sanitize отложил бы закрывашки за дивайдер, оставив frag_a с перекосом). Мутационно-релевантен — падал бы ровно при той будущей регрессии эпика, что я флагнул. Не тавтология. - **Do#2 (устаревшие комменты):** 4 коммента обновлены «whole-feed (HTML)» → «per-post в `_render_pipeline` для ОБОИХ (RSS и HTML)» (`post_parser.py` + `rss_generator.py`). - **Адверсариально чисто:** дельта = только новый тест (+50) + коммент-правки. Goldens НЕ тронуты → полный `pytest tests/` **347 passed** (346 + 1 новый §3.4) на тех же фикстурах ⇒ прод-поведение неизменно (комменты behavior-neutral). Нового бага нет. Остальное (security fail-closed + безопасный superset + нет cross-post-bleed; golden-аудит: каждое изменение §3-объяснено, ноль потери контента, §3.4 чинит латентный баг; stability loop-safety+thread-safe; coherence) — неизменно с round-1, всё LGTM. DROP-заметки round-1 (hr-тест покрыт golden'ом, CSS `expression()` до-существующий) в силе. <sub>Фикс-раунд: оба Do закрыты, §3.4-инвариант заперт мутационно-релевантным тестом (падал бы при join-before-sanitize), комменты актуализированы, гейт 347 passed, goldens не тронуты. Этап 1 эпика #34 чист.</sub> <!-- state:review reviewed_head=711cb2cd31 round=2 verdict=pass -->
agent_reviewer added review/approved and removed review/needs labels 2026-07-06 17:03:24 +03:00
vvzvlad merged commit 17b5fcd1e4 into refactor/render-stage0-golden 2026-07-06 17:05:21 +03:00
Sign in to join this conversation.