Files
houseplan-card/docs/reviews/CODE-REVIEW-57-r1.md
T
claude[bot] fe15d863ce
Validate / docs (push) Successful in 22s
Validate / provenance (push) Successful in 41s
Validate / process-gate (push) Failing after 49s
Validate / changes (push) Successful in 38s
Validate / hacs (push) Skipped
Validate / hassfest (push) Skipped
Validate / frontend (push) Skipped
Validate / smoke (push) Skipped
Validate / golden (push) Skipped
Validate / performance_smoke (push) Skipped
Validate / backend (push) Skipped
docs: code review document for #57
Issue: #57
User-Visible: no
2026-08-19 00:58:46 +00:00

264 lines
23 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# CODE-REVIEW-57-r1
- **Issue:** https://github.com/Matysh/houseplan-card/issues/57
- **ТЗ:** `docs/specs/057-color-opacity-picker.md` (ревью
`docs/reviews/SPEC-REVIEW-57-r1.md`, вердикт — зелёный, Medium-1 вынесен в
#180, Low-1 — устранён автором прозой §9)
- **Диапазон:** `git diff origin/dev...HEAD` / `git log --oneline
origin/dev..HEAD` — коммиты `ffb1084` (ТЗ), `32e3a79` (документ спек-ревью),
`299da59` (реализация, `Issue: #57`, `User-Visible: yes`)
- **Роль:** ревьюер кода (не автор), этап `S7-code-review`
- **Цикл:** r1/4
## Скоуп ревью
Проверялось соответствие реализации:
- ТЗ `docs/specs/057-color-opacity-picker.md` — контракту §7–§14 (единая
поверхность, значения/преобразования, pointer/touch, keyboard/a11y, API/
migration, AC1–AC9);
- `docs/SCOPE.md` — задача не расширяет продукт за пределы editor usability
polish;
- `AGENTS.md`/`PROCESS.md` — классы файлов, трейлеры, оба changelog в одном
`User-Visible: yes` коммите, отсутствие новых runtime dependencies;
- фактическому поведению `src/hp-color-opacity.ts`, `src/color-picker.ts`,
`src/floating-surface-controller.ts`, вызовам в `src/houseplan-card.ts`;
- реально выполненным гейтам (см. ниже), включая один прогон golden-сьюта
целиком, не только названного в ТЗ сценария.
## Как проверялось
1. Прочитан весь diff (`git diff origin/dev...HEAD --stat` и файлы по
отдельности): `src/hp-color-opacity.ts` (412 изменённых строк),
новый `src/color-picker.ts` (80 строк, чистые функции конверсии),
изменения `src/houseplan-card.ts` (8 call sites получили
`.pickerLabels=${this._colorPickerLabels}`), `src/i18n/{en,ru}.json` (6 новых
пар ключей `color_picker.*`), `test/color-picker.test.mjs`,
`demo/smoke_color_picker.mjs`, `demo/smoke_help_affordance.mjs` (+3 строки),
`docs/{CHANGELOG.md,CHANGELOG.ru.md,USER-GUIDE.md,USER-GUIDE.ru.md,TESTING.md}`.
2. Подтверждено построчным поиском `<hp-color-opacity` в `houseplan-card.ts` и
тестом-сканом (`test/color-picker.test.mjs:48-49`), что все 8 call sites
(decor stroke/fill/text/furniture, room/space custom fill, marker Glow)
получили новый компонент и локализованные `pickerLabels` — совпадает с §12
ТЗ и списком, ранее сверенным в SPEC-REVIEW-57-r1.
3. Прочитан `src/hp-color-opacity.ts` целиком: подтверждено отсутствие
`<input type="color">`, единая `.picker` поверхность
(`role="dialog"`, `aria-label`) рендерится синхронно с saturation/value
полем, hue/saturation/value ranges, hex-полем и (при `showOpacity`) alpha
row+процентом — соответствует §7/§14 AC1 ТЗ дословно.
4. Прочитан `src/color-picker.ts`: `normalizeHexColor` принимает 3/6-значный
hex с необязательным `#`, `rgbToHsv`/`hsvToRgb`/`hsvToHex` — конечные,
clamped функции без побочных эффектов — соответствует §8 ТЗ.
5. Прочитан `FloatingSurfaceController` (`src/floating-surface-controller.ts`)
и его использование в `hp-color-opacity.ts` (`_floating`,
`registerOverlay`, `_renderFallback`, `_supportsPopover`) — подтверждает
заявленное в ТЗ переиспользование общей инфраструктуры #68, а не
параллельную реализацию.
6. Прочитан весь текст issue #57 и всех комментариев (`gh issue view 57
--comments`): исходный запрос, аналитика с Q1, решение владельца по Q1
(«один клик — одна поверхность с цветом и прозрачностью, без вложенного
system picker»), финальный хендофф автора с цифрами бандла и списком
выполненных проверок. Реализация воспроизводит решение владельца дословно
(см. «Что проверено и корректно»).
7. Прогнаны все обязательные дешёвые гейты и часть гейтов «по необходимости»
(перечень и результаты — раздел «Гейты» ниже), включая полный
`npm run golden:verify` — diff меняет видимый рендер во всех диалогах,
использующих `hp-color-opacity`, что подпадает под критерий «геометрия/
стили/слои» из инструкции к этому ревью.
8. Для каждого теста, который прогонялся, целенаправленно сломан
соответствующий кусок продукта и подтверждено, что тест падает, затем код
восстановлен и гейты перепрогнаны на чистом дереве (см. «Гейты»).
9. Просмотрены PNG-артефакты (`artifacts/golden/actual/*`,
`artifacts/golden/diff/*`) для всех пяти сценариев, показавших `different`,
чтобы отличить реальную визуальную регрессию от ожидаемого следствия новой
поверхности (см. находку Low-1).
10. Проверены трейлеры и class-принадлежность:
`git show 299da593 --stat` — класс A/B/C/D-файлы согласованы (продукт +
smoke/test + документация + все три копии бандла в одном коммите),
`Issue: #57`, `User-Visible: yes`, оба changelog в том же коммите.
11. Проверено отсутствие новых runtime dependencies:
`git diff origin/dev...HEAD -- package.json package-lock.json` — пусто.
12. Проверено, что Medium-1 из SPEC-REVIEW-57-r1 действительно заведён
отдельным issue: `gh issue view 180` существует, метки `P3`/`tech-debt`/
`S1-new`, ссылается на #57.
## Обязательные гейты (всегда)
| Гейт | Команда | Результат |
|---|---|---|
| Typecheck | `npx tsc --noEmit` | чисто, без вывода |
| Unit-тесты | `npm test` | `884/884`, 0 fail |
| Build + сверка бандлов | `npm run build` + `sha256sum dist/houseplan-card.js custom_components/houseplan/frontend/houseplan-card.js demo/srv/assets/houseplan-card.js` | все три идентичны, `6d7e2b02aee1a3af822f1f634209a0fd62e1348988b87c7a1a4eff0f08fa7490` — совпадает (без учёта регистра) с заявленным автором в хендоффе |
Falsifiability (тест должен уметь падать), применена к обоим прогнанным
наборам:
- временно вставлен `<input type="color">` в `hp-color-opacity.ts` →
`test/color-picker.test.mjs` тест «contains no nested native color picker»
корректно упал (`not ok 50`); код восстановлен, `npm test` снова 884/884.
- временно превращён `_setHue` в no-op → `demo/smoke_color_picker.mjs`
корректно провалил `hueUpdatesSharedDraft` и `shiftArrowUsesTenStep`; код
восстановлен, бандл пересобран, три копии совпадают с исходным SHA-256,
`git status` чист.
Bundle budget (AC8): raw `1 107 696 → 1 119 200` (`+11 504 B`, совпадает с
заявленным автором), gzip (`gzip -9`) `304 081 → 306 755` (`+2 674 B`) —
близко к заявленным автором `+2 728 B` (разница из-за конкретного
gzip-инструмента/уровня в отчёте, не в самом бандле); в любом случае это
на порядок меньше бюджета ТЗ `≤15 KiB gzip`. Runtime dependencies не
добавлены (`package.json`/`package-lock.json` не изменены).
## Гейты по необходимости
| Гейт | Почему запускался | Результат |
|---|---|---|
| `node demo/smoke_color_picker.mjs` | назван в AC1–AC5 ТЗ как способ доказательства | зелёный, все 14 проверок `true`; falsifiability подтверждена (см. выше) |
| `node demo/smoke_help_affordance.mjs` | тронут (`+3` строки) для проверки color-only Glow и forced-fallback exclusivity с `hp-help`, назван в AC6/§15 ТЗ | зелёный, все проверки `true`, включая `fallbackPickerFocusable` (forced-fallback ветка для пикера) и `glowColorOnlyHasNoAlpha` |
| `npm run golden:verify` (полный набор, 66 активных сценариев в текущей матрице) | diff меняет видимый рендер (новая поверхность вместо `input[type=color]`) во всех диалогах, использующих `hp-color-opacity`, — подпадает под критерий «рендер/геометрия/стили» из инструкции к ревью, а не только названный в ТЗ сценарий | 61 passed, 5 `different` — все пять разобраны построчно, см. находку Low-1 |
| `node scripts/check-docs.mjs --external` | дёшево, диф трогает 4 файла документации | зелёный, «7 files, 10 external links» |
| `node scripts/process-gate.mjs --issues` | дёшево, проверяет трейлеры/структуру коммитов диапазона | зелёный, «гейт пройден, предупреждений 0» |
## Что проверено и корректно
- **Главный контракт владельца по Q1 воспроизведён точно.** Один
click/tap/Enter/Space по `.trigger` открывает единственную `.picker`
поверхность; внутри нет `<input type="color">` и второго dialog — прямым
чтением кода и тестом-сканом, а не только smoke.
- **Все 8 call sites обновлены единообразно** и не получили адаптеров: decor
stroke/fill/text/furniture, room/space custom fill, marker Glow — тот же
список, что был подтверждён на этапе ТЗ. `showOpacity=false` для Glow
по-прежнему скрывает только alpha-row (`ranges().length === 3` в smoke).
- **API и данные не расширены.** `color`/`opacity`/`showOpacity`/`disabled` и
событие `hp-color-opacity-change` с прежней формой `{ color, opacity }` —
без изменений сигнатуры; ни один файл backend/`types.ts`/`custom_components`
не тронут (AC9 — подтверждено `git diff --stat`).
- **Round-trip и валидация значений (AC3)** — table-тест по всему цветовому
кубу подтверждает допуск ≤1 канал; невалидный hex-драфт никогда не
эмитится (`invalidHexNeverEmits` в smoke), commit восстанавливает последнее
валидное значение — прочитано в `_commitHex` и подтверждено выполнением.
- **Touch/pointer (AC4)** — `setPointerCapture`/`releasePointerCapture`,
`pointercancel`/`lostpointercapture` откатывают черновик без нового
события (`pointerCancelAddsNoEvent` в smoke); второй одновременный pointer
игнорируется (`_activePointerId !== null` guard) — проверено чтением кода,
не отдельным multi-touch smoke (см. «Чего не проверял»).
- **Keyboard/a11y (AC5)** — все четыре ranges имеют `aria-label`/
`aria-valuetext`, `step=1` даёт шаг 1, `Shift+Arrow` даёт шаг 10
(`shiftArrowUsesTenStep` в smoke); hex-поле — подписанный text input с
`aria-invalid`/`aria-describedby` без live-спама по символам; Escape
закрывает picker и возвращает фокус на trigger без закрытия диалога
(`escapeClosesFirstAndRefocuses`); focusable-состав включает pickerButton в
фокус-трапе диалога и в forced-fallback ветке (`shadowFocusables`,
`fallbackPickerFocusable`).
- **Floating lifecycle (AC6)** — общий `FloatingSurfaceController`
используется без дублирования логики #68; forced-fallback ветка (когда
`showPopover`/`hidePopover` недоступны) реально рендерит picker в теневой
портал и остаётся focusable — подтверждено выполнением
`smoke_help_affordance.mjs`, не только чтением.
- **i18n** — новые ключи `color_picker.{title,hue,saturation,value,hex,
invalid_hex}` добавлены синхронно в `en.json`/`ru.json`; компонент остаётся
presentation-only (`pickerLabels` — свойство, не читает `hass.locale`
напрямую) — `cardLanguageOwnsCopy` в smoke подтверждает изоляцию по
`config.language` на уровне карточки.
- **Trailers/классы файлов/changelog** — коммит `299da59` несёт `Issue: #57`,
`User-Visible: yes`; оба changelog (`docs/CHANGELOG.md`,
`docs/CHANGELOG.ru.md`) редактируются в этом же коммите; ни новых runtime
dependencies, ни изменений `package.json`.
- **Medium-1 из SPEC-REVIEW-57-r1 закрыт корректно** — issue #180 существует,
правильно размечен и ссылается на #57; реализация не пытается тайно
расширить или сузить скоуп относительно принятого ТЗ.
- **Bundle budget (AC8)** — независимо пересобранный бандл даёт тот же
SHA-256 и raw-дельту, что заявил автор; gzip-дельта на порядок меньше
лимита `≤15 KiB` независимо от точного инструмента измерения.
## Находки
### Low-1 — реализация меняет визуальный рендер ещё в 4 golden-сценариях, не названных нигде в ТЗ/TESTING.md как ожидающие ревью
**Файлы:** `docs/TESTING.md:69-89` (раздел «Unified color and opacity picker
(#57)», называет только `decor-color-popover-mobile-ru`); фактически
затронуты также `device-dialog-desktop-en`, `device-dialog-mobile-ru`,
`device-help-popover-light-ru`, `tray-narrow-tool-ru`.
Полный прогон `npm run golden:verify` (66 активных сценариев в текущей
матрице) даёт 5 `different`, не 1: помимо названного в TESTING.md
`decor-color-popover-mobile-ru`, отличаются ещё `device-dialog-desktop-en`,
`device-dialog-mobile-ru` (marker Glow trigger — новый 40×40 swatch/picker
вместо старого layout), `device-help-popover-light-ru` (тот же Glow trigger в
соседстве с `hp-help`) и `tray-narrow-tool-ru` (decor color trigger в узкой
панели инструментов). Просмотр `artifacts/golden/actual/*.png` для всех пяти
подтверждает: рендер корректен, не обрезан, не сломан — разница целиком
объясняется новым размером/видом trigger-кнопки (`checkerboard` 40×40)
против прежнего инлайн `input[type=color]` + `range`, что каскадно сдвигает
контент диалога на 12–20 CSS px вниз. Это ожидаемое и безопасное следствие
явно принятого технического решения (§6 ТЗ), не дефект поведения.
**Почему это находка, а не просто ожидаемое поведение.** `docs/TESTING.md`
(единственное место, которое перечисляет пре-бета golden-обязательства этой
задачи) называет только один сценарий. Тот, кто перед бетой примет решение
`golden:accept` только по названному списку, рискует пропустить, что
реальный список отличающихся baseline — впятеро шире. Пре-бета гейт (полный
`golden:verify`) всё равно покажет все 5 расхождений и не даст принять
неполный набор молча — риск чисто в неполноте самодокументирования задачи,
не в скрытом от процесса дефекте.
**Решение ревьюера:** Low, не блокирует `S8-merged`. Не требует отдельного
issue — это не новая функциональность и не дефект поведения, а неполнота
одной строки в `docs/TESTING.md`, целиком описывающей последствия этой же
задачи. Оставляю с этой записью, правка необязательна: `golden:verify`
перед бетой в любом случае предъявит все 5 сценариев ревьюеру беты, а не
только названный.
## Чего не проверял
- **Multi-touch (второй одновременный pointer) как отдельный smoke** — §9
ТЗ требует, чтобы второй pointer «отменял первый либо игнорировался
безопасно»; `demo/smoke_color_picker.mjs` проверяет только
`pointercancel` одного pointer. Ветка «второй pointer игнорируется»
(`_svPointerDown`: `if (this._activePointerId !== null) return;`)
проверена чтением кода, не исполнением — как и просил бы обычный, не
light-track процесс при отсутствии реального multi-touch harness в
окружении ревью.
- **200% браузерный zoom на 390 CSS px (AC7/§10 ТЗ)** — ни один smoke, ни
один golden-сценарий (`demo/golden/matrix.mjs`) не эмулирует
page-level 200% zoom; существующие `zoom: 0.4/2.5` в матрице — это
масштаб плана внутри канваса, не zoom страницы. CSS-контракт
(`width: min(292px, calc(100vw - 16px))`, `overflow: auto`) читает как
корректный, но это не исполненное доказательство для этой конкретной
строки AC7 — фиксирую честно, а не выдаю чтение кода за тест.
Не блокирует: остальные AC7-грани (light/dark, checkerboard, отсутствие
прыжка layout при disabled) подтверждены выполнением/чтением.
- **`golden:accept`** не запускал и не должен — принятие новых baseline
(для всех 5 «different», не только названного) принадлежит пре-бета
этапу (`PROCESS.md`, `npm run golden:accept -- --reviewed` только на
полном Linux CI-артефакте), не циклу код-ревью.
- **`python -m pytest tests_backend -q`** — не запускал: diff не трогает ни
одного файла `custom_components/**/*.py` (подтверждено `git diff --stat`),
гейт неприменим.
- **performance-профили** (`benchmark:*`) — не запускал: AC явно не
называет производительность как критерий (только «no new long task» в
§15 плана автотестов, не в нумерованных AC1–9), diff не трогает canvas/
рендер-цикл плана — rAF-коалессинг в pointermove-пути прочитан в коде
(`_queueHsvEmit`) и оценивается как корректный по конструкции, но не
измерен профайлером.
- **Полный построчный обзор `src/houseplan-card.ts` (~18500 строк)** на
предмет иных мест, не относящихся к call sites `hp-color-opacity` —
не требуется этим циклом: контракт ТЗ (после Medium-1 → #180) явно
ограничен уже перечисленным списком, и `test/color-picker.test.mjs:48`
формально фиксирует ожидаемое число (8) вызовов компонента.
## Вердикт
Зелёный. High: 0, Medium: 0, Low: 1 (не блокирует, запись оставлена в этом
документе, правка необязательна — см. Low-1). Реализация корректно и
проверяемо закрывает контракт ТЗ и решение владельца по Q1: единая
поверхность цвета и прозрачности без вложенного системного picker, на всех
восьми существующих call sites, без изменения API/схемы/данных и без новых
runtime dependencies, в рамках заявленного бюджета bundle size. Все
обязательные гейты и относящиеся к тронутым поверхностям smoke/golden гейты
выполнены лично ревьюером (не переиспользована декларация автора), включая
собственноручную проверку «тест умеет падать» для одного unit- и одного
browser-smoke теста. Единственная находка — неполнота одной строки в
`docs/TESTING.md` относительно фактического числа golden-сценариев,
меняющихся вместе с этой задачей — не блокирует, поскольку пре-бета гейт
сам по себе предъявит полный список расхождений до `golden:accept`.