20 KiB
CODE-REVIEW-179-r1
- Issue: https://github.com/Matysh/houseplan-card/issues/179
- ТЗ:
docs/specs/179-device-icons-redesign.md(зелёное SPEC-REVIEW r2) - Диапазон:
origin/dev..HEAD, коммиты517a710…7af6d74, основной —48bcdaf(«feat: redesign device marker faces») - Ревьюер: Claude (свежая сессия, без контекста реализации)
- Вердикт: зелёный · цикл r1/4 · High: 0 · Medium: 0
Скоуп
Единый shell/core-рендерер маркера устройства (пакет дизайнера #179) на полном
плане, editor preview и static space card: geometry/theme, приоритет
состояний, Text/Double без ellipsis, категориальный marker LQI, motion
timings/цвета, keyboard/hit-area/aria, backward-совместимый config. Диапазон
ограничен ровно тем, что перечислено в хендоффе; посторонних правок не найдено
(git diff --stat origin/dev...HEAD — только файлы из плана ТЗ §13 плюс
docs/i18n/tests).
Как проверялось
| Гейт | Команда | Результат |
|---|---|---|
| Typecheck | npx tsc --noEmit |
green |
| Unit | npm test |
930 tests, 930 pass, 0 fail, 0 skip |
| Build + bundle parity | npm run build && cmp (все 3 копии) |
green, побайтово идентичны |
| Mutation guards (новые, не полный набор) | node scripts/mutation-gate.mjs --id=device-unavailable-hover-restored / --id=device-marker-lqi-low-boundary-shifted / --id=device-long-value-ellipsis-restored / --id=device-keyboard-bypasses-click-path |
все 4: чистый прогон зелёный, мутант красный — «тест умеет падать» подтверждено для каждого |
| Целевые browser smokes (названные в ТЗ §16.2) | node demo/smoke_device_icon_design.mjs, smoke_device_preview_parity.mjs, smoke_static_icon.mjs, smoke_state_value.mjs, smoke_disabled_device.mjs |
все green |
Secure-action smokes (lock invariant, docs/SCOPE.md CR-1) |
smoke_lock_action.mjs, smoke_lock_invariant.mjs, smoke_toggle_confirmation.mjs |
все green |
| Смежный smoke, задетый styles.ts (continuous-пульсация cover) | smoke_cover_tap.mjs |
green |
| Golden (диагностика, не acceptance) | node demo/golden/run.mjs --mode=capture --scenario=device-icon-state-table-light / -dark |
missing-baseline (ожидаемо, как и заявил автор), рендер без runtime-ошибок, изображения сверены визуально — см. «Проверено и корректно» |
--check реестра мутаций |
node scripts/mutation-gate.mjs --check |
все патчи, включая 4 новых, ложатся ровно один раз |
Гейты НЕ прогонялись, и почему:
- Полный
node scripts/mutation-gate.mjs(без--id) и полный browser-smoke набор (127 файлов) — задача касается общего рендерера маркера, но не всех 127 поверхностей; PROCESS.md §8 прямо резервирует полный прогон за предрелизным гейтом. Прогнаны все смоки, названные в ТЗ §16.2, плюс secure-action и один смежный (cover), которые diff трогает напрямую. npm run golden:verify(полный набор, 100+ сценариев) — golden-политика (demo/golden/policy.mjs) сама отказывается гонятьverifyчастично; полный прогон — Linux CI перед бетой (canon,AGENTS.md). Вместо этого выполнена диагностикаgolden/run.mjs --mode=captureтолько по двум новым сценариям — видимый результат сверен глазами (см. ниже), это не замена gate, а проверка «не сломано визуально».python -m pytest tests_backend -q— Python-код (custom_components/**) не тронут (только doc-комментарийvalidation.pyбыл прочитан для сверки диапазонаripple_size, не изменён).- Performance-профили (
performance_smoke.mjs) — AC13 не называет числовой бюджет для прогона именно на этом ревью; код прочитан на отсутствие per-markerResizeObserver/matchMedia/backdrop-filter(ниже) вместо измерения. Профиль остаётся предрелизным гейтом (ТЗ §16.4).
Находки
Нет. High: 0, Medium: 0. Ниже — два Low, оба решены на месте (не требуют правки, оставлены с записью).
- Low-1 (снят).
src/styles.ts, блок.dev(~строка 1875):--device-core-bgи--device-core-fgобъявлены дважды подряд в одном правиле — сначала черезvar(--card-background-color…)/var(--primary-text-color…), затем тем же свойством черезlight-dark(#fff, #252525). Второе объявление всегда выигрывает (последнее правило для custom property в одном селекторе), первое — мёртвый код. Функционально не влияет:.dev.theme-light/.dev.theme-darkвсё равно переопределяют обе переменные явно, когдаhass.themes.darkModeизвестен (см.deviceThemeClass,smoke_device_icon_design.mjs—lightThemeCoreIsWhite/darkThemeCoreIs252525подтверждают оба пути). Косметика, не блокирует. - Low-2 (снят). Цветовая линия
tone-humidity(border-color: #4fc3f7), различавшая temperature/humidity legacy-сателлиты бордюром, не имеет преемника в новом shell — все секции (value badge, LQI-как-badge, legacy temp/hum) теперь визуально одинаковы (белая/тёмная пилюля без цветной рамки). Это не регрессия относительно ТЗ: §8.2 прямо требует единый shell без отдельных «спутников», а пакет дизайнера не определяет per-tone цвета — унификация здесь является сутью задачи, а не побочным эффектом. Отмечаю для протокола, правки не требую.
Проверено и корректно (по каждому AC)
- AC1 (shell/theme).
src/styles.ts—--device-shell-size: calc(var(--dev-size) * 1.26875)=101.5/80,--device-shell-padдаёт0.788core/shell с учётом 1px stroke — совпадает с ТЗ §7.1. Light:stroke #BCBCBC,box-shadow: 0 1px 2px rgb(37 40 45/12%), 0 4px 8px -1.07px rgb(37 40 45/18%),backdrop-filter: noneявным правилом. Dark:--device-core-bg:#252525через класс.dev.theme-dark. Подтверждено смоком (sharedShellGeometry,packageShadowColor,noBackdropBlur,lightThemeCoreIsWhite,darkThemeCoreIs252525— всеtrue). unit + smoke. - AC2 (states/priority). CSS-каскад разложен так, что правила
.dev.alarm…идут физически последними среди правил с равной специфичностью (.dev.sel,.dev:focus-visible,.dev.alarm), что при равенстве специфичности (по 3 класс-эквивалента на каждый селектор) даёт порядок Alert > Focus > Selected по исходному коду — соответствует нормативному приоритету §7.3. Golden-диагностикаdevice-icon-state-table-{light,dark}визуально подтверждает: alarm-маркер (одновременноsel+alarm+focus) рисуется красным поверх всех остальных decorations. unit (presentation matrix, не показан здесь, но покрытdevice-presentation.test.mjs) + golden-диагностика. - AC3 (unavailable).
.dev:not(.unavail):hoverисключает.unavailиз hover-перекраски и shell-border; клик/keyboard остаются разрешены (_clickDeviceне проверяет unavailable). Мутантdevice-unavailable-hover-restoredкрасный при восстановлении общего hover — проверил лично, гейт ловит регрессию. unit + smoke, мутация подтверждена исполнением. - AC4 (secure locks).
lockStateвычисляется только дляlock.*entity (не путается с generic cover/contactopen),lock-lockedдаёт чёрный glyph на theme-фоне,lock-unlocked— amber#F0A00C. Keyboard путь (_keyDevice→_clickDevice) — тот же самый метод, что pointer click, без отдельной ветки для секьюрных действий.smoke_lock_action.mjsиsmoke_lock_invariant.mjs(полный набор secure-инвариантов изdocs/SCOPE.mdCR-1) — оба green, обход подтверждения не найден. unit + secure-action smoke, лично прогнано. - AC5 (virtual/static).
.dev.virtual .device-shell { border-style: dashed }— только shell, не core (не путается с реальным unavailable).static_iconполучает theme-aware core/shell, но не реагирует на live state/pulse/value (resolveDevicePresentationподставляет нейтральныйvisualдляstaticIcon, не тронуто в этом diff).smoke_static_icon.mjsgreen. unit + smoke. - AC6 (Text/Double).
deviceTextScale()— детерминированная функция без DOM-измерения (юнит подтверждает0.45для короткого,0.25-floor для длинного);overflow: visible,text-overflowне установлен. Shell расширяется по content (width: max-contentна core/badge). Единыйdevice-sectionsконтейнер для badge + legacy secondary (третья секция). Мутантdevice-long-value-ellipsis-restoredкрасный при восстановленииellipsis— проверил лично.textIsComplete/doubleUsesOneShellв smoke — true. unit + smoke + мутация, всё исполнено. - AC7 (LQI).
markerLqiBand/markerLqiColor— границы<=40/<180/>=180дают ровноlow/mid/highна{0,40,41,179,180}(юнит-тест по всем пяти граничным значениям).logic.ts::lqiColor()(комнатный градиент) не импортируется больше вdevice-presentation.tsдля маркера, но используется по-прежнему для заливки комнаты — не тронут. Мутантdevice-marker-lqi-low-boundary-shifted(сдвиг<=40→<40) красный — проверил лично. unit + мутация, исполнено. - AC8 (motion). Timings/easing сверены с ТЗ §10: continuous
3.6s cubic-bezier(.45,.05,.55,.95), short1.1s×3, delay 0/1.1/2.2, cubic-bezier(.22,.61,.36,1), alarm2.4s, 2 волны с задержкой 1.2s, тот же easing, scale1→1.5.semanticPulseColor()даёт green/presence, amber/running|working|open, blue/иначе — юнит-тестdevice-pulse.test.mjsпроверяет все три ветки плюс explicit-override приоритет. unit (исполнено) + smoke (presenceContinuousGreentrue). - AC9 (reduced motion).
reducedMotionIndicator: 'dot'заменяет кольца;.activity-dotберёт цвет из того же--ripple-color, что и кольца (раньше был явныйnullпод reduced motion — терялся цвет точки, теперь исправлено). Одна host-levelmatchMediaподписка (_motionMedia,houseplan-card.ts), в device-слое новых подписок не добавлено (grep подтверждает). unit + проверено чтением (нет второй подписки). - AC10 (keyboard/a11y).
role="button"/tabindex="0"только когдаmode === 'view' || 'devices';planNotInTabOrderв smoke подтверждает отсутствие атрибутов в Plan-режиме. Preview (hp-device-preview.ts) и static card (space-render.ts) получилиrole="img"без tabindex — не входят в tab order (grep подтверждает отсутствиеtabindexв обоих файлах). Enter/Space вызывают тот же_clickDevice, что click (мутантdevice-keyboard-bypasses-click-pathкрасный при подмене пути — проверил лично).aria-labelвключаетstate_a11y_*/lqi_a11y_*—unavailableAriaIsReadableв smoke подтверждает наличие "unavailable" и "LQI/signal" в одной строке. Hit-area 44×44 — через.dev::beforecmax(44px, shell-size),hitAreaAtLeast44true. Возврат фокуса после диалога — существующий, не тронутый этим diff механизмhp-dialog.ts(connectedCallbackзахватывает opener,disconnectedCallbackего восстанавливает); маркер теперь фокусируем, поэтому механизм автоматически распространяется без нового кода — проверено чтением, не отдельным тестом (в ТЗ явно «по существующему dialog contract», не новая фича). unit + smoke + мутация (частично исполнено, частично чтением). - AC11 (parity). Один
renderDeviceFace()вызывается из всех трёх поверхностей (houseplan-card.ts,hp-device-preview.ts,space-render.ts) — новых копий DOM/state-mapping не найдено (grep поrenderDeviceFace— 3 вызова, все из общего модуля).smoke_device_preview_parity.mjsиsmoke_static_icon.mjsgreen. existing smoke. - AC12 (совместимость).
ripple_sizeдефолт3→1.5заменён консистентно во всех 4 местах записи/чтения конфига (grep подтверждает отсутствие старого литерала3); "не материализуется" — та же тернарная схема!== 1.5 ? value : null, что и раньше была!== 3, проверено чтением каждого из 4 мест (нет посвящённого юнит-теста на этот конкретный литерал — проверено чтением, не исполнением, отмечаю честно). Backendvol.Range(min=1, max=20)(validation.py) не менялся; UI-слайдер расширен2→1, что внутри уже разрешённого backend-диапазона, не нарушение. i18n en/ru parity — новые ключи присутствуют в обоих файлах построчно. unit (частично) + чтение кода + backend-схема прочитана. - AC13 (производительность).
grep ResizeObserver— только существующие host/viewport подписки, ни одной новой per-marker;grep backdrop-filter— единственное вхождениеnoneна.device-shell(запрещающее правило, соответствует Dark No-Blur решению M4 из SPEC-REVIEW r1). Motion — толькоtransform/opacity(не задевает layout). SVG-примеры пакета не импортированы вsrc//dist(grep по характерным именам файлов архива — пусто). проверено чтением, не исполнением; performance_smoke — предрелизный гейт, не гонялся здесь. - AC14 (release artifacts).
docs/CHANGELOG.md/.ru.md,docs/USER-GUIDE.md/.ru.md,docs/ARCHITECTURE.md,docs/TESTING.mdобновлены в том же коммите48bcdaf(User-Visible: yes), терминология USER-GUIDE.ru.md согласована с уже принятой в файле («жёлтая подложка» и т.п., не изобретена заново). i18n en/ru добавлены parity. Golden matrix version31→32, новый юнитgolden-matrix.test.mjsпроверяет структуру двух новых сценариев. Скриншоты (docs/images/*.png,screenshots.json) обновлены вместе с кодом.
Трейлеры и процесс
- Каждый из 7 коммитов диапазона несёт
Issue: #179и ровно одинUser-Visible: yes|no;yes— только на48bcdaf, где лежат оба changelog. Остальные 6 —no(доки/тесты/ТЗ), корректно.node scripts/process-gate.mjs --issues— green по заявлению автора; офлайн-часть (--check, структура трейлеров) визуально сверена поgit log, расхождений не найдено. - Ветка
issue/179-device-icons-redesignсоответствует номеру issue. Диапазон коммитов не содержит посторонних правок вне плана ТЗ §13.
Итог
Реализация соответствует ТЗ по всем 14 AC, включая явно принятые владельцем
решения (§4) и технические предположения (§20). Секьюрный инвариант замков
(docs/SCOPE.md CR-1) не затронут — подтверждено прогоном
smoke_lock_invariant.mjs/smoke_lock_action.mjs. Все 4 новых mutation-guard
лично прогнаны и подтверждённо ловят свою регрессию — это самое сильное
доступное в этом процессе доказательство «тест умеет падать» для AC3/AC6/AC7/AC10.
Golden-baseline осознанно не принят (корректно оставлено предрелизному Linux
CI гейту); диагностический прогон двух новых сценариев не выявил визуальных
дефектов. Два Low-замечания зафиксированы и сняты без правки кода (обоснование
выше). High: 0, Medium: 0 — задача уходит в очередь на пре-релиз.