# CODE-REVIEW-211-r1 — визуальное соответствие маркеров дизайн-пакету #179 - **Issue:** https://github.com/Matysh/houseplan-card/issues/211 - **ТЗ:** `docs/specs/211-device-icons-visual-parity.md` (принято `SPEC-REVIEW-211-r2`, зелёный) - **Диапазон реализации:** `6feb018..270cf63` - `4e82976` fix: match device icons to designer package · `Issue: #211` · `User-Visible: yes` - `270cf63` test: strengthen unavailable hover mutant · `Issue: #211` · `User-Visible: no` - **Цикл:** r1/4 ## Скоуп проверки Диапазон реализации ограничен двумя коммитами: `src/styles.ts` (geometry/theme/state tokens), новый независимый reference-fixture (`demo/srv/reference/device-icons/**`), расширенный `demo/smoke_device_icon_design.mjs`, новый `demo/capture_device_icon_reference.mjs`, точечная правка мутационного теста `scripts/mutation-gate.mjs`, три копии бандла, оба changelog, `docs/TESTING.md` и шесть скриншотов `docs/images/*`. Ни `device-visual.ts`, ни `device-presentation.ts`, ни `device-pulse.ts`, ни `device-face.ts` не тронуты — совпадает с §10 ТЗ («не должны меняться без доказанного отсутствующего renderer fact»). Конфиг, i18n, `custom_components/**/*.py` не задеты. ## Как проверялось Проверка велась не по описанию хендоффа, а самостоятельным прогоном на чистом дереве и собственным чтением диффа/скриншотов. ### Гейты — прогнаны ``` npx tsc --noEmit → чисто, без ошибок npm test → 939 pass, 0 fail, 0 skipped npm run build → OK cmp dist vs custom_components/.../houseplan-card.js → идентичны cmp dist vs demo/srv/assets/houseplan-card.js → идентичны node scripts/check-docs.mjs → "7 files, 10 external links" — OK node demo/smoke_device_icon_design.mjs → все 29 фактов true, OK node demo/smoke_device_preview_parity.mjs → все 15 фактов true, OK node demo/smoke_static_icon.mjs → все 12 фактов true, OK node demo/smoke_disabled_device.mjs → все 7 фактов true, OK node scripts/mutation-gate.mjs --id=device-unavailable-hover-restored → чистый прогон зелёный; мутант красный, «поймано 1 из 1» node demo/capture_device_icon_reference.mjs → сгенерирован artifacts/device-icon-reference/device-icons-reference-runtime.{html,png} node demo/golden/run.mjs --mode=capture --scenario=device-icon-state-table-light node demo/golden/run.mjs --mode=capture --scenario=device-icon-state-table-dark → оба "different" (ожидаемо, старый baseline зафиксировал баг) ``` Независимая проверка «тест умеет падать» (дисциплина применена к самому дорогому и самому content-heavy тесту, `smoke_device_icon_design.mjs`): в отдельном `git clone` собран бандл со **старым** `src/styles.ts` (коммит `6feb018`, до фикса) и **новым** смоком/reference-фикстурой (`270cf63`). Результат: **13 фактов красные** — `geometryMatchesAt32_56_96`, `referenceStateMatrixMatches`, `referenceHoverMatches`, `virtualUsesThemeDefaultWithDashedShell`, `iconCoreIsCircle`, `valueBadgeIsPill`, `activeUsesPackageAmber`, `lightActiveUsesWhiteGlyphAndAmberShell`, `darkDefaultShellUsesPackageStroke`, `darkHoverChangesCoreOnly`, `selectionDecoratesCoreNotShell`, `focusDecoratesCoreNotShell`, `darkLockUsesWhiteGlyphAndDarkShell`. Это больше, чем девять фактов, названных в хендоффе (хендофф перечислял по смыслу, не по имени предиката) — расхождение не в пользу автора, а в сторону более сильного доказательства: тест ловит регрессию с запасом. AC9 подтверждён независимо, не на слово. ### Визуальная проверка «глазами» (обязательна по §11 ТЗ) Просмотрен `artifacts/device-icon-reference/device-icons-reference-runtime.png` целиком (28 строк: Default/Hover/Active/Lock/Unlock/Selected/Focus/Alert/ Virtual/Unavailable/Text/Double Right для Light и Dark, плюс Default на 32/56/96 px). Круглая геометрия, размер MDI-глифа, value-pill радиус, theme stroke и цвета состояний в runtime визуально совпадают с колонкой Reference SVG построчно, включая Lock/Unlock (проверены отдельным 4×-кропом — форма и пропорция глифа замка совпадают с эталоном в обеих темах). Также открыт и прочитан `artifacts/golden/actual/device-icon-state-table-{light,dark}.png` целиком. Комбинация `alarm + sel` с `focusDevice` на одном маркере («golden-climate») отрендерена так: core и shell — красные (alert выигрывает покраску, как требует §7.3), а кольцо декорации — синее (focus), не янтарное (selected). Это прямое следствие принятого в #179 приоритета `Alert > Focus > Selected > Hover > semantic > Default`: кольцо — теперь отдельный CSS-слой (`--device-ring-color`/`--device-ring-width` на `.device-core`), и правило `:focus-visible` в каскаде идёт позже `.sel`, то есть более приоритетное состояние перекрывает кольцо менее приоritetного — ровно то поведение, которого требует таблица приоритета. До этого коммита кольцо было частью `box-shadow` на `.device-shell` и `.dev.alarm` (более специфичное правило ниже по каскаду) стирало декорацию selection/focus полностью — то есть alert раньше «съедал» кольцо целиком, а не выигрывал только цвет core/shell. Новое поведение (alert красит core/shell, но не гасит более высокоприоритетный focus-ring) точнее реализует буквальное требование §7.1 «Selected/Focus ring — отдельный круглый слой; не заменяет внешний shell» и не описано ни одним найденным мной артефактом как регрессия. Технически это расширение видимой информации (можно увидеть, что элемент одновременно и alert, и focused), не потеря её. Проверены также обновлённые документационные скриншоты (`docs/images/01-view-desktop.png` и другие) — маркеры на них уже круглые, а не скруглённо-квадратные; `sourceFingerprint` в `screenshots.json` пересчитан и совпадает с проверкой `check-docs.mjs`. ### Коммиты и трейлеры Оба коммита несут `Issue: #211`. `4e82976` (`User-Visible: yes`) в одном коммите правит оба changelog (`docs/CHANGELOG.md`, `docs/CHANGELOG.ru.md`) — условие AGENTS.md выполнено. `270cf63` (`User-Visible: no`) правит только `scripts/mutation-gate.mjs` — чистый class B, трейлер соответствует. ## Что проверено и корректно - **AC1 (геометрия).** Круглый core (`border-radius: 50%` вместо `28%`), shell/core `1.26875` сохранён через `box-sizing: border-box` и компенсирующий `--device-shell-pad` независимо от переменной ширины stroke — геометрия не плывёт между состояниями с разной толщиной обводки. Value-pill радиус — `height/2` (`0.39375`), а не `0.18`. `geometryMatchesAt32_56_96` подтверждает допуск на 32/56/96 px, включая painted bbox настоящего `mdi:lightbulb-spot` (реальный SVG-путь, не bounding box `ha-icon`). - **AC2 (theme parity).** Light/Dark default core/glyph/stroke/shadow и запрет `backdrop-filter` подтверждены смоком и визуальной таблицей; `darkDefaultShellUsesPackageStroke` сверяет буквальный `rgba(37,37,37,0.75)`. - **AC3 (state parity).** Default/Hover/Active/Lock/Unlock/Alert сверены байт-в-байт с прямым SVG (цвет **и** ширина stroke) в обеих темах; Selected/Focus/Virtual/Unavailable подтверждены отдельными вычисляемыми фактами и визуально. Тёмный Unlock корректно использует утверждённый владельцем янтарный override, а не архивный зелёный. - **AC4 (combinations).** Golden-сценарий `device-icon-state-table-*` содержит маркер с `alarm+sel+focus` одновременно; результат просмотрен и соответствует принятому в #179 приоритету слоёв (см. выше). - **AC5 (layouts).** Text/Double/third section/LQI не сломаны (`doubleUsesOneShell`, `textIsComplete`, `lqiBandsProjected`). - **AC6 (surface parity).** `smoke_device_preview_parity` и `smoke_static_icon` зелёные — план, preview и static card используют общий `renderDeviceFace()` без расхождений. - **AC7 (interaction regression).** 44×44 hit area, unavailable click/tap без hover, keyboard/tab order, secure-путь — все факты зелёные, `smoke_disabled_device` не задет. - **AC8 (data/semantics).** Диапазон реализации не касается `device-visual.ts`/`device-presentation.ts`/`device-pulse.ts`, i18n или `custom_components/**`; unit-suite (939) зелёный без исключений. - **AC9 (failing-before-fix).** Независимо подтверждено (13 фактов красные на старом CSS, все зелёные после фикса) — см. раздел «Как проверялось». - **AC10 (release artifacts).** Оба changelog, `docs/TESTING.md`, обновлённые скриншоты — всё в user-visible коммите; golden не принят вручную (верно — ждёт полного reviewed Linux CI artefact). ## Находки Нет находок уровня High или Medium. Два Low, оба не блокируют и не требуют правки в этом цикле: - **L1.** `virtualUsesThemeDefaultWithDashedShell` в смоке проверяет только состояние `virtual` в покое; «virtual hover идентичен ordinary hover» (§7.3 ТЗ) не покрыт отдельным вычисляемым фактом — только логическим чтением каскада (правило `.dev:not(.unavail):hover` не имеет исключения для `.virtual` и не переопределяется ничем более специфичным). Риск минимален и ограничен одним недостающим предикатом в уже большом смоке. Снимаю без правки: поведение не изменилось этим диапазоном (правила `.virtual`/`.dashed` не тронуты диффом), это пробел покрытия, а не дефект поведения. - **L2.** Комбинации `Alert+Focus+Selected` не сверены с одноимённым SVG архива буквально (§7.3 формально требует «сверяются с одноимёнными Light SVG»), потому что такого экспортированного файла в переданном архиве нет — собственный AC4 ТЗ признаёт это, требуя доказательство именно «combination golden», которое выполнено и просмотрено. Снимаю без правки: доказательство соответствует явно заявленному в ТЗ уровню, более сильного источника истины не существует. Отдельно фиксирую техническую неточность в хендоффе, не влияющую на вердикт: он сообщает «938 pass, 1 skipped», а независимый прогон на этом же дереве даёт «939 pass, 0 skipped» (совпадает с `npm run inventory`). Не поднимаю как находку — расхождение не воспроизводится и не меняет результат ни одного гейта. ## Чего не проверял и почему - **Полный набор из 152 browser smokes** — не запускал; диапазон реализации ограничен общим рендерером маркера и его собственным smoke/preview/static покрытием, названным в AC и хендоффе. Остальные 148 не имеют отношения к геометрии/theme/state маркера. - **`npm run golden:verify` по полному набору сценариев** — не запускал; прогнал `capture` только по двум сценариям, прямо относящимся к задаче (`device-icon-state-table-{light,dark}`), и открыл получившиеся PNG. Полный golden-прогон и оценка остальных ~90 сценариев на предмет побочных визуальных отличий из-за глобального изменения CSS маркера — предрелizный гейт (PROCESS.md §8), а не гейт код-ревью; риск того, что какой-то другой golden-сценарий с устройствами теперь тоже "different", реален, но это ожидаемое и штатно обрабатываемое следствие правки, а не повод возвращать задачу на этом этапе. - **`python -m pytest tests_backend -q`** — не запускал; диапазон не затрагивает `custom_components/**/*.py`. - **Performance-профили** — не запускал; правка не добавляет DOM-узлов, layout read или подписок (`device-face.ts` не тронут), а AC не называет performance-смок явно. - **Полный Linux CI-прогон** — не запускал локально; это гейт CI при выходе из код-ревью, не задача рецензента. ## Вердикт Все десять AC выполнены и подтверждены либо автотестом, который доказанно умеет падать, либо непосредственным визуальным просмотром, как того требует §11 ТЗ. Регрессий в интеракции, данных, конфиге и семантике не найдено. **Зелёный.**