# CODE-REVIEW — issue #51 «Custom decor images» · заход r4 - Issue: https://github.com/Matysh/houseplan-card/issues/51 - Материал раунда: `git diff 043aa0d826cf1e1490a02feb4cc28f50cb96245b..HEAD` (материал r3 объявлен в `docs/reviews/CODE-REVIEW-51-r3.md`). - SHA материала: `f7dfc6cab2dd02b19e19e222b981c85fd7c9b66b` (сверено с `git rev-parse HEAD` — совпадает). - SHA предыдущего раунда (r3): `043aa0d826cf1e1490a02feb4cc28f50cb96245b` — жив, `git cat-file -t` → `commit`, `git merge-base --is-ancestor 043aa0d8 HEAD` подтверждает: прямой предок `HEAD`, ребейза не было. - Трейлеры обоих коммитов дельты (`6bfa42ca`, `f7dfc6ca`) — `Issue: #51`, `User-Visible: no`. Корректно: ни один не меняет продуктовый код или наблюдаемое пользователем поведение (только `tests_backend/**` и `demo/smoke_gear_tabs.mjs`) — правок в `docs/CHANGELOG*` не требуется и не сделано. ## Скоуп раунда Единственная предметная находка r3 (Medium, в скоупе: backend coverage ratchet красный из-за недопокрытого `decor_assets.py`, 71%) закрыта одним коммитом (`6bfa42ca`). Второй коммит раунда (`f7dfc6ca`) — попутная стабилизация ранее упавшего в CI `demo/smoke_gear_tabs.mjs`, тест-инфраструктура, не связанная с #51 по существу, но входящая в тот же материал. Дельта — 2 файла (без бандлов, без учёта опубликованного `docs/reviews/CODE-REVIEW-51-r3.md`, который положил конвейер, а не автор): `tests_backend/test_decor_assets.py` (+71 строка, новые тесты) и `demo/smoke_gear_tabs.mjs` (+12/-4, замена таймаута на `page.waitForFunction`). Продуктовый код (`src/**`, `custom_components/houseplan/**` за пределами тестов) не тронут вообще. Ребейза на ушедший вперёд `dev` не было, контракт поведения не менялся, новая подсистема не задета, объём дельты (2 файла, только тесты) на порядок меньше исходной задачи — критерий «разбор остаётся полным» не выполняется, сокращение объёма правомерно. Разбирал дельту построчно (diff показан целиком выше), не декларативно. ## Закрытие раунда r3 | Находка r3 | Чем закрыта | Где это видно | |---|---|---| | Finding 1 (Medium, в скоупе): `pytest tests_backend` с coverage-ratchet красный на `043aa0d8` — `decor_assets.py` покрыт на 71%, ниже среднего проекта; `coverage: 86.9%` при `baseline 87.2%` | `6bfa42ca` добавил 8 новых тестов в `test_decor_assets.py`: реальные валидные JPEG/WebP(VP8/VP8L/VP8X)-фикстуры для полного декодирования через Pillow (`test_supported_raster_headers_and_full_decode`), граничные условия размера/расширения upload (`test_upload_size_and_extension_guards`), SVG dimensions/viewBox/text edge cases (`test_svg_dimension_and_text_boundaries_fail_closed`), пустой каталог и путь метаданных (`test_catalog_empty_directory_and_metadata_path`), malformed spaces/asset-id в reference scan (`test_reference_scan_skips_malformed_spaces_and_asset_ids`) | Независимо перепроверил на реальном CI-логе job'а «Бэкенд: pytest в Home Assistant» прогона [33685974303](https://github.com/Matysh/houseplan-card/actions/runs/33685974303) (SHA `6bfa42ca`, backend job **success**): `custom_components/houseplan/decor_assets.py 292 32 132 22 87%` (было 71% на `043aa0d8`), `TOTAL … 87%`, `coverage: 87.5% (baseline 87.2%)` — ratchet зелёный, `534 passed, 2 skipped`. Дополнительно прогнал сам локально: `python3 -m pytest tests_backend/test_decor_assets.py -q` → `35 passed` (совпадает с заявленным автором числом), `ruff check tests_backend/test_decor_assets.py` → чисто | Прогон 33685974303 в целом завершился `failure` не из-за backend-а, а из-за независимого падения `Смоки в браузере (шард 3 из 3)` — это и есть триггер второго коммита раунда (`f7dfc6ca`, `smoke_gear_tabs`). Проверил отдельно: на итоговом `HEAD` (прогон [33686693823](https://github.com/Matysh/houseplan-card/actions/runs/33686693823), уже названный в контексте задачи как зелёный Validate) шард 3 содержит `ok smoke_gear_tabs`; job «Бэкенд: pytest в Home Assistant» в этом прогоне **skipped**, потому что джоб «Переиспользование: это дерево уже проверено» законно переиспользовал побайтово идентичный backend-результат прошлого зелёного прогона (диапазон коммита `f7dfc6ca` не трогает backend-релевантные пути) — это не пропуск гейта, а корректное переиспользование того же самого прогона, который я сверил выше напрямую по логу. ## Проверено и корректно - **Новые тесты не декларативны.** Построчно сверил каждый новый тест с реализацией в `custom_components/houseplan/decor_assets.py` (`validate_asset`, `_validate_svg`, `asset_refs`, `asset_meta_path`, `read_catalog`): все `pytest.raises(..., match=...)` соответствуют реальным сообщениям об ошибках и реальным веткам кода (`_check_size`, `ext not in ASSET_EXTENSIONS`, `viewBox`-парсинг на 4 значения, `_validate_dimensions` safety limit, SVG text-branch, `root.is_dir()` false-branch, `isinstance`/regex-guard в `asset_refs`). Проверил сам, что тест умеет проходить не «вхолостую»: локальный прогон зелёный (35/35), плюс независимое подтверждение по логу реального CI на backend job с ростом покрытия именно целевого модуля. - **Coverage ratchet — монотонный, не занижен.** `scripts/backend-coverage-baseline.txt` не менялся в этой дельте (`git diff 043aa0d8..HEAD -- scripts/backend-coverage-baseline.txt` пуст), остаётся `87.2`; фактическое покрытие выросло до `87.5%` за счёт реальных тестов, а не понижения планки — соответствует монотонному контракту гейта (docs/specs/042-backend-engineering-quality.md §4). - **`smoke_gear_tabs.mjs`-правка не ослабляет проверку.** Сравнил diff: утверждение `out.kioskNoAddButton = !sr.querySelector('.tab.tabadd')` не изменилось; заменён только механизм ожидания — `page.waitForFunction` на появление `ha-card` в shadow root вместо гонки `setTimeout(350)` + форсированного переприсваивания `hass`. Т.к. Lit рендерит шаблон компонента одним синхронным проходом, появление `ha-card` в DOM означает завершение того же render()-прохода, в котором решается наличие `.tab.tabadd` — проверка становится детерминированной, а не слабее. Остальная часть файла (`res`-блок с выравниванием шестерёнок, диалогом создания, mobile-блок) не тронута. - **Трейлеры и провенанс.** Оба коммита дельты несут `Issue: #51`, `User-Visible: no` — корректно, видимое поведение не меняется. Job «Предполётные проверки» (провенанс коммитов, процессный гейт) в прогоне 33686693823 — success. - **User-Visible.** Нет изменений в `docs/CHANGELOG*` — и не требовалось: диффа `src/**`/`custom_components/**` продуктового кода нет. ## Унаследовано из r1–r3 (без повторной проверки) Дельта раунда не касается ничего из перечисленного ниже — переносится по документам предыдущих раундов, SHA которых подтверждены как прямые предки `HEAD` (см. цепочку merge-base выше и в `CODE-REVIEW-51-r3.md`): - **AC1–AC4, AC6, AC8–AC13, AC16** (продуктовый контракт: upload/palette, furniture-parity transforms без wall magnet, raster pipeline, reusable lifecycle, missing-repair, import/export, i18n, unified projection) — из `docs/reviews/CODE-REVIEW-51-r1.md`, SHA `38205d87`, доказаны автотестом либо разобраны чтением. - **High r1** (SVG DTD/entity billion-laughs, UTF-16-обход байтового префильтра) — закрыт `encoding-aware expat.ParserCreate()`, адверсариально перепроверен на 6 кодировках/вариантах в `docs/reviews/CODE-REVIEW-51-r2.md`, SHA `042af520`. - **Medium r1 (3 шт.)** — `smoke_decor.mjs` на актуальном runtime API, единая `projectDecorImage` (устраняет «одно число — два источника» между full/static-рендером), positive/negative resolve-кэш — все три закрыты и перепроверены в `CODE-REVIEW-51-r2.md`, SHA `042af520`. - **Low r1 (2 шт.)** — touch pointerType для snap, SVG per-attribute bounds — закрыты там же; третий Low (data:/javascript: substring) сознательно оставлен как принятый риск, запись сохраняется. - **Medium r2** (устаревший `scripts/config-schema.json`) — закрыт, перепроверен в `docs/reviews/CODE-REVIEW-51-r3.md`, SHA `043aa0d8` (`git diff` после регенерации пуст). - **Гейты, зелёные на `043aa0d8` и не тронутые этой дельтой:** `tsc --noEmit`, `npm test` (1777 passed/1 skipped), `npm run build`, `bundle:sync`, `bundle:budget` (291018/300000, известный долг #367), `no-new-any`, `check-docs.mjs --external` (отпечаток документационных скриншотов), `ruff check custom_components/houseplan`, golden/model-invariants (дельта их не касается — ни разу не тронуты с r1, продуктовый код без изменений). - Model invariants (`npm run invariants`) не нужны ни в этом раунде, ни накопительно — diff всей задачи не создаёт новых ссылок на геометрию комнат/стен сверх уже проверенного в r1. ## Что проверил лично в этом раунде - `git diff 043aa0d8..HEAD` — весь, построчно (2 файла, не считая опубликованного документа r3). - `python3 -m pytest tests_backend/test_decor_assets.py -q` — 35 passed (локально, после `pip install pytest defusedxml Pillow`). - `python3 -m ruff check tests_backend/test_decor_assets.py` — чисто. - Реальные логи CI: job «Бэкенд: pytest в Home Assistant» прогона 33685974303 (SHA `6bfa42ca`) — построчная таблица покрытия, `decor_assets.py` 87%, `TOTAL` 87.5% vs `baseline 87.2%`, `534 passed, 2 skipped`; job «Смоки в браузере (шард 3 из 3)» прогона 33686693823 (SHA `f7dfc6ca`, тот самый Validate) — `ok smoke_gear_tabs`. - `node scripts/smoke-select.mjs --base 043aa0d8 --head HEAD` → «Исполняемого frontend-диффа нет (`src/**/*.ts` не тронут). Browser-smoke этим диффом не выбираются — выбирать нечего». - `git diff 043aa0d8..HEAD -- scripts/backend-coverage-baseline.txt` — пусто, планка не занижена. - `git merge-base --is-ancestor 043aa0d8 HEAD` — подтверждён прямой предок, ребейза не было. ## Что не проверял и почему - **Полный `tsc --noEmit` / `npm test` / `npm run build` / сверка трёх копий бандла заново** — не прогонял: диффа `src/**` в этом раунде нет, а Validate на точном `HEAD` (`f7dfc6ca`, прогон 33686693823) уже зелёный по этим шагам (см. контекст задачи) — повторный прогон не добавляет информации. - **Полный `pytest tests_backend`/HA-harness заново** — не перезапускал весь набор (нет пиновой версии Python 3.14/HA в песочнице ревьюера); вместо этого прочитал реальный лог зелёного backend job на точном SHA фикса (`6bfa42ca`) и убедился по построчной таблице покрытия, что находка r3 закрыта числом, а не декларацией. Backend job в финальном Validate-прогоне (`f7dfc6ca`) законно skipped как переиспользование идентичного backend-результата — проверил это переиспользование, а не принял на слово. - **`golden:verify`, model invariants, performance-профили** — не прогонял: дельта раунда не трогает `src/**`, рендер, геометрию комнат/стен или производительные пути; этот вывод не новый — он наследуется от всех предыдущих раундов, где diff этих областей тоже не касался. - **`check-docs.mjs`** — не перезапускал: диффа `src/**` нет, отпечаток документационных скриншотов от этого раунда зависеть не может. - **`smoke-select` по остальным ~65 smoke-файлам** — не прогонял: сам инструмент подтвердил «выбирать нечего» для этой дельты (frontend-код не тронут); `smoke_gear_tabs.mjs`, единственный изменённый smoke-файл, подтверждён напрямую логом реального CI, а не инструментом выбора. - **`node scripts/no-new-any.mjs`** — не перезапускал: дельта не содержит TypeScript. ## Вывод Единственная находка r3 закрыта предметно и подтверждена числом (не заявлением автора): покрытие `decor_assets.py` выросло с 71% до 87%, общий coverage ratchet зелёный (87.5% vs baseline 87.2%, планка не занижена) — подтверждено чтением реального CI-лога на точном SHA фикса. Побочная стабилизация `smoke_gear_tabs.mjs` не ослабляет проверяемый инвариант и подтверждена тем же реальным CI на финальном SHA. Новых находок в дельте r4 нет. High: 0, Medium: 0. --- ## Материал раунда - Ветка: `issue/51-custom-decor-images`, коммит `f7dfc6cab2dd` — ребейз его осиротит, и это нормально: ниже якоря, которые ребейз не меняет. - Дерево материала: `0fdd0e0043cec7cd4015dbcae6bc7949c4b64167` ``` git log --all --format='%H %T' | grep 0fdd0e0043ce ```