# Code review #219 — r1 - **Issue:** https://github.com/Matysh/houseplan-card/issues/219 - **Spec:** `docs/specs/219-lock-orange-palette.md`, зелёное `SPEC-REVIEW-219-r2.md` - **Reviewed branch:** `issue/219-lock-orange-palette` - **Reviewed range:** `origin/dev..HEAD` = `28d6b9c`…`84fa434` (ТЗ и ревью ТЗ, уже приняты на этапе spec) → `bc75e00` (единственный продуктовый коммит) - **Base:** `origin/dev` at `b56e122` - **Reviewer:** Claude, независимая сессия без контекста реализации ## Вердикт **Зелёный · цикл r1/4 · High: 0 · Medium: 0.** Единственный продуктовый коммит `bc75e00` заменяет старую black/amber lock-палитру на red/green и унифицирует foreground всех цветных подложек (`on`/`open`) по теме. Изменение точечное и декларативное — только `src/styles.ts` — и покрыто исполняемыми contract-тестом и browser-smoke, которые я лично прогнал, а contract-тест дополнительно проверил на способность падать. Все 7 AC закрыты либо автотестом, либо чтением кода с явной пометкой. Одна Low-находка снята без правки. ## Скоуп Продуктовый файл — `src/styles.ts`: - `.oplock.locked`/`.oplock.unlocked` (compact opening lock badge) и `.dev.lock-locked`/`.dev.lock-unlocked` (обычный marker) получили core/stroke `#66D17A`/`#F0410C` вместо `light-dark(#000,#252525)`/`#F0A00C`; foreground переведён на общий паттерн `light-dark(#fff,#252525)` + explicit `.theme-light`/`.theme-dark` overrides, как уже сделано у `.dev.on`; - `.dev.open` получил тот же foreground-паттерн вместо постоянного `#4a2800`, плюс явные `.dev.theme-light.open`/`.dev.theme-dark.open`; - ни geometry, ни stroke-ratio, ни selector order, ни alarm/hover/focus/ selected/unavailable/virtual/press-правила не тронуты — весь остальной файл вне двух изменённых блоков идентичен `dev`. Сопутствующие изменения: `test/device-marker-polish-contract.test.mjs` (новый contract-тест issue 219), `demo/smoke_device_icon_design.mjs` (новый `openingLockPaletteMatches`/`orangeGlyphsFollowTheme`/ `darkLockUsesGreenCoreAndDarkGlyph`, обновлённый reference override), `demo/capture_device_icon_reference.mjs` + `demo/srv/reference/device-icons/README.md` (runtime-override red/green поверх byte-identical designer SVG), `demo/golden/matrix.mjs` + `test/golden-matrix.test.mjs` (сцена `device-icon-state-table-*` теперь показывает `open` рядом с `on` и обоими lock-состояниями), `docs/TESTING.md`, `docs/USER-GUIDE.md` + `docs/USER-GUIDE.ru.md` (раздел «Замок» и таблица постоянных статусов — заменяемый black/amber текст убран), оба changelog, `docs/specs/README.md`, `docs/images/screenshots.json` (обновлён только `sourceFingerprint`, все 10 `imageSha256` без изменений), три сгенерированные копии бандла. Ровно один продуктовый коммит `bc75e00`, трейлеры `Issue: #219` / `User-Visible: yes` на месте, оба changelog в том же коммите, документация (USER-GUIDE, TESTING) в том же коммите, что и поведение. Ветка `issue/219-lock-orange-palette` соответствует правилу именования. ## Как проверялось | Гейт | Результат | |---|---| | `npx tsc --noEmit` | pass | | `npm test` | **962/962 pass**, 0 skipped в этом окружении (`npm run inventory` подтверждает канонический счётчик 962 Node unit); хендофф указывает «961 passed, 1 skipped» — расхождение объяснимо `test/process-gate.test.mjs`, где 1 подтест условно `t.skip('нужен исполняемый stub gh')`/«git недоступен» в зависимости от окружения автора, файл не затронут этим диффом | | `npm run build` + сверка трёх копий бандла | pass, все три `sha256` = `9c7df844ce825c0d56cfb65e82a0fbbb98620428d5aa5806abec0c40b1d9b1a2`, совпадает с хендоффом | | `node --test test/device-marker-polish-contract.test.mjs` (targeted contract, назван в AC1/AC2/AC3) | pass (8/8) | | Временный откат `--oplock-core-bg: #66D17A` → `#000` и повтор того же теста | **тест `issue 219 gives locks red-open green-closed…` красный** — дисциплина «тест умеет падать» подтверждена мной, а не только на слово автора | | `node --test test/golden-matrix.test.mjs` | pass (26/26), включая новую проверку `deviceClassOverrides['golden-right-linkquality'] === ['open']` | | `node demo/smoke_device_icon_design.mjs` (назван в AC1/AC3, хендоффе) | pass, все поля true, включая новые `openingLockPaletteMatches`, `orangeGlyphsFollowTheme`, `darkLockUsesGreenCoreAndDarkGlyph` | | `node demo/smoke_device_preview_parity.mjs` (поверхность AC5 — Device preview) | pass | | `node demo/smoke_static_icon.mjs` (поверхность AC5 — static space card) | pass | | `node demo/smoke_lock_action.mjs` (смежная поверхность — lock action не должен был измениться) | pass | | `node demo/smoke_lock_invariant.mjs` (смежная поверхность — lock invariant/SCOPE.md guard) | pass | | `node scripts/check-docs.mjs --external` (AC7) | pass, 7 файлов, 10 внешних ссылок | | `npm run golden:verify` | `device-icon-state-table-light`/`-dark` — **different**, ожидаемо (см. ниже); плюс 3 сцены, не названные в ТЗ — разобраны отдельно | ### Разбор `golden:verify`: почему «different» больше, чем в ТЗ ТЗ (§10, §15, §16) называет ревьюеру только `device-icon-state-table-light`/ `-dark` как ожидаемую визуальную дельту. Фактический прогон показал ещё три «different»: `isometric-large-warm-remount-dark`, `large-house-zoom-250-dark`, `large-house-warm-remount-dark` (у всех пяти одинаковый `maxObservedDelta: 218` — общий признак одного и того же цветового изменения). Я не принял это на слово и сравнил с чистым `origin/dev` в отдельном `git worktree` (та же сборка, тот же `golden:verify`): на `dev` эти три сцены проходят как `passed`, а `device-icon-state-table-*` уже были `different`/ `missing-baseline` **до** этой задачи — предсуществующий дрейф baseline (`GOLDEN_MATRIX_VERSION` 32→33 внесён более ранним `39456dc`, эталоны не приняты), не имеющий отношения к #219. Причина трёх новых «different» — `demo/fixtures/large-house.mjs` строка 129 циклически включает `['lock', 'locked']` в синтетический набор устройств, которым пользуются `perf-floor-1`/`perf-floor-2`; палитра lock-marker в этих сценах меняется тем же общим CSS, что и ожидалось для shared renderer (AC5). Это не регрессия и не «постороннее» изменение из риска в §13 — только более широкий, чем задокументировано, набор golden-сцен, которым в предрелизном гейте потребуется `npm run golden:accept -- --reviewed` вместе с state-table. `device-text-shell-long-light/-dark` — `missing-baseline` независимо от этой задачи (тот же предсуществующий дрейф `dev`, issue #217, вне скоупа #219). ### Не прогонялось, и почему - **Полный набор из 155 browser-smoke.** Задача — точечное изменение общих CSS state-токенов; прогнаны названные в AC/хендоффе плюс смежные поверхности (device preview parity, static card, lock action, lock invariant). Остальные 150 смоков не относятся к lock/orange-подложке (проёмы без замков, Glow, wall geometry, UI-хром и т. д.). - **`python -m pytest tests_backend`.** Ни один файл `custom_components/**/*.py` не тронут. - **Performance-профили.** Не названы в AC; §12 ТЗ явно фиксирует «CSS variables и fixture/test metadata», без нового DOM/JS в render path. ## Проверка AC1–AC7 | AC | Метод по ТЗ | Статус | Как закрыт | |---|---|---|---| | AC1 | contract unit + light/dark visual artifact | ✅ | `device-marker-polish-contract.test.mjs` проверяет `.oplock.locked/unlocked` core/stroke/fg по обеим темам; artifact — `smoke_device_icon_design.mjs` `openingLockPaletteMatches` (реальный `.oplock-core`/`.oplock-shell` computed style против reference с runtime-override) | | AC2 | presentation/unit source contract + shared-renderer test + golden state-table | ✅ | Тот же contract-тест проверяет `.dev.lock-locked/unlocked` идентичными hex теми же селекторами, что и `.oplock.*`; `houseplan-card.ts`/`hp-device-preview.ts`/`space-card.ts` подтверждены источником как использующие общий `cardStyles`; golden `device-icon-state-table-*` включает оба lock-состояния | | AC3 | contract unit + state-table light/dark golden с одновременно видимыми `on`/`open` | ✅ | Contract-тест проверяет `.dev.open` не содержит `#4a2800` и использует `light-dark(#fff,#252525)`; smoke `orangeGlyphsFollowTheme` подтверждает `rgb(255,255,255)`/`rgb(37,37,37)` для `on` и `open` в обеих темах на реальном рендере; golden-сцена теперь содержит `open` (`golden-right-linkquality`) рядом с `on` | | AC4 | существующие device presentation/polish/pointer tests + source review | ✅ (чтением) | Diff `src/styles.ts` ограничен двумя блоками (`oplock.locked/unlocked`, `dev.open/lock-locked/lock-unlocked`); alarm/hover/focus/selected/unavailable/virtual/press/pulse селекторы вне диффа побайтово идентичны `dev`; полный regression (962/962) не покраснел | | AC5 | shared-renderer unit + golden matrix | ✅ | Contract-тест проверяет, что `houseplan-card.ts`, `hp-device-preview.ts`, `space-card.ts` импортируют `cardStyles`; smoke `smoke_device_preview_parity.mjs`/`smoke_static_icon.mjs` подтверждают parity на реальных DOM для plan/preview/static; golden `device-icon-state-table-{light,dark}` — одна fixture, обе темы | | AC6 | diff review, typecheck, полный unit и build | ✅ | Diff не касается config/backend/i18n (нет файлов вне `src/styles.ts`, тестов, demo, docs, generated); typecheck/unit/build зелёные | | AC7 | diff, hash comparison, `check-docs` | ✅ | Оба changelog, `docs/TESTING.md`, `docs/USER-GUIDE.md`+`.ru.md` (раздел «Замок», таблица «Жёлтая подложка»/«Чёрный значок замка» переписаны на red/green) — все в `bc75e00`; три bundle sha256 идентичны; `check-docs.mjs --external` pass; `sourceFingerprint` в `docs/images/screenshots.json` обновлён и совпадает с фактическим build fingerprint, все 10 `imageSha256` не изменились | ## Находки ### Low-1 — AC7/§16 не называют все golden-сцены, которые изменит эта правка ТЗ и хендофф называют только `device-icon-state-table-light`/`-dark` как ожидаемую golden-дельту. Фактически общий CSS задевает ещё три сцены (`isometric-large-warm-remount-dark`, `large-house-zoom-250-dark`, `large-house-warm-remount-dark`) через lock-устройства в синтетической `large` fixture (см. разбор выше) — я подтвердил это сравнением с чистым `dev`, а не предположением. Риск §13 «Golden покажет ожидаемую дельту вместе с посторонней» ровно это и предвидел, возлагая проверку на ревьюера — что я и сделал. **Вердикт:** снимается без правки. Не блокирует AC7 (диапазон изменяемых сцен явно не был ограничен списком) и не создаёт риска для релиза — я проверил происхождение всех пяти «different» и подтвердил, что это одно и то же корректное цветовое изменение, а не регрессия. Фиксирую для предрелизного гейта: `npm run golden:accept -- --reviewed` должен принять **все пять** сцен разом (`device-icon-state-table-{light,dark}` + `isometric-large-warm-remount-dark` + `large-house-zoom-250-dark` + `large-house-warm-remount-dark`), не только две названные в ТЗ. `device-text-shell-long-{light,dark}` (`missing-baseline`) — предсуществующий дрейф `dev` вне скоупа #219, не путать с этой правкой при приёмке. ## Что проверено и корректно - `.oplock.locked`/`.oplock.unlocked` и `.dev.lock-locked`/`.dev.lock-unlocked` используют идентичные hex (`#66D17A`/`#F0410C`) и идентичный `light-dark`+explicit-override паттерн foreground — одна lock-сущность даёт одинаковый цвет как compact badge, так и обычный marker (риск §13 «Compact opening badge и device marker разойдутся» закрыт одним contract-тестом на обе пары). - Исходные designer SVG (`demo/srv/reference/device-icons/**`) остаются byte-identical; runtime-override в `capture_device_icon_reference.mjs` подтверждён построчным чтением фактических `fill="black"`/`fill="#252525"`/ `fill="#1DC21D"`/`fill="#F0A00C"` — каждое встречается в SVG ровно один раз, `replaceAll` не задевает ничего постороннего. - `--oplock-stroke-ratio: .025`/`--device-shell-stroke-ratio: .025` (dark theme) и вся геометрия/MDI-path/hit-area не изменены — совпадает с «не входит в задачу» §7 ТЗ. - `mdi:lock`/`mdi:lock-open-variant` classification не менялась (regex-проверка в contract-тесте против `houseplan-card.ts`). - `docs/USER-GUIDE.md`/`.ru.md`: заменяемые формулировки («чёрный/тёмный замок», «жёлтая подложка» применительно к lock, «Чёрный значок замка») убраны полностью, включая таблицу постоянных статусов и `lock.*` в общей таблице поведений — сверено построчно, не на слово хендоффа. - Trailers, changelog RU/EN, `docs/specs/README.md` — все в одном продуктовом коммите, соответствуют PROCESS.md §7.1/§10.1. ## Чего не проверял - Визуальный итог обновлённых golden-сцен (baseline не принимается исполнителем/ревьюером по контракту — только `npm run golden:accept -- --reviewed` на полном Linux CI artifact перед бетой). - Полный набор из 155 browser-smoke и `performance_smoke` — не относятся к этому точечному CSS-изменению; остаются обязательными на предрелизном гейте. - Backend/HA harness — не затронут, ни один файл `custom_components/**/*.py` не изменён. - Мобильный/touch путь отдельно не тестировал: правка ограничена CSS-цветом, не добавляет и не убирает pointer-modality селекторы (подтверждено чтением диффа — новых `:hover`/`[data-pointer-*]` правил нет).