Files
houseplan-card/docs/reviews/CODE-REVIEW-476-r2.md
T
2026-09-06 12:56:14 +00:00

26 KiB
Raw Blame History

CODE-REVIEW-476-r2

  • Issue: #476 — Явное завершение выбора цвета кнопкой «ОК»
  • Этап: код-ревью (PROCESS.md §2.7), заход r2, блокирующих циклов израсходовано 1 из 4
  • Ветка: issue/476-color-picker-ok, HEAD 6e0e1f8c (origin/dev = fc5973c2, ровно merge-base — ветка полностью приведена к dev, ребейз чист, конфликтов нет)
  • Диапазон: git log --oneline origin/dev..HEAD — 9 коммитов; материал — git diff origin/dev...HEAD, 56 файлов
  • ТЗ: docs/specs/476-color-picker-ok.md, полный трек, ревью ТЗ зелёное на r2 (docs/reviews/SPEC-REVIEW-476-r2.md)

Почему разбор полный, а не по дельте

Между r1 код-ревью (жёлтый, docs/reviews/CODE-REVIEW-476-r1.md, доказано на HEAD b5a1001e) и этим заходом ветка дважды двигалась:

  1. автор устранил M1 и M2 коммитом test: harden color picker confirmation guards — локальная правка тестов/реестров, ожидаемый предмет обычной дельты;
  2. затем origin/dev ушёл вперёд ещё на 5 коммитов, review не запустился («Ревью не запускалось» — конфликт слияния, цикл не израсходован), и автор перебазировал ветку на fc5973c2, разрешив конфликт только в сгенерированных dist/**/custom_components/houseplan/frontend/** (пересобраны из объединённых исходников).

Пункт 2 — ребейз на ушедший вперёд dev, что PROCESS.md §2.10/§7.2 прямо называет «другим кодом», обязывающим к полному разбору. Я его сделал: ниже — полная таблица гейтов и AC, а не только диф вокруг M1/M2. Практически это означало не доверять чужому заявлению «конфликтовали только сгенерированные файлы», а проверить самому: git log --oneline origin/dev..HEAD -- src/hp-color-opacity.ts показывает ровно один коммит (82f65acd, реализация, предшествующая r1-ревью) — второй ребейз действительно не тронул продуктовую логику, только сгенерированные бандлы и docs/images/screenshots.json (fingerprint). Это подтверждено чтением, а не принято на слово.

О «SHA не назван» (проверка §2.10, шаг 1)

Комментарий-вердикт r1 в issue (11:47:29Z) сам по себе не называет проверенный SHA — но материал по процессу объявляется не в комментарии, а в блоке «Материал раунда» документа (CODE-REVIEW-476-r1.md), и там SHA есть: в прозе документа — HEAD b5a1001e (после того как конвейер сам привёл ветку к dev, "34a6b276 → b5a1001e"), в машинном блоке — коммит 34a6b276473c (тот же материал, тег до этого приведения) плюс дерево 94485fdc0eca.... Отсутствие SHA в комментарии — не находка: PROCESS.md §2.10 явно возлагает эту обязанность на документ, а не на комментарий.

Оба анкера (34a6b276473c, b5a1001e) сейчас не резолвятся в этом чекауте (git cat-file -t 94485fdc0eca... → could not get object info) — это следствие второго ребейза (пункт 2 выше), случившегося после публикации документа r1 (12:11 против 11:47). На момент публикации r1 материал был жив, что и требуется §2.10; это обычный случай осиротевшего SHA («98 из 804»), не находка. Проверить содержимое напрямую я не мог, поэтому опираюсь на: (а) единственный коммит, трогающий src/hp-color-opacity.ts, не менялся между r1 и сейчас (см. выше), (б) построчное сравнение текста файла ниже совпадает с тем, что описывает документ r1.

Скоуп диффа (полный)

src/hp-color-opacity.ts (61 строка, коммит 82f65acd, не менялся после r1) — полноширинная кнопка «ОК», _hexNeedsValidInput защита от обхода невалидного HEX повторным подтверждением, _confirm() со stopPropagation(). src/houseplan-card.ts (1 строка) — confirm label. src/i18n/{en,ru,de,fr}.json — color_picker.confirm. test/color-picker.test.mjs, test/i18n.test.mjs — юниты на DOM-порядок/CSS/label/i18n-parity. demo/smoke_color_picker.mjs (расширен), demo/smoke_help_affordance.mjs (fallback-путь), новый demo/smoke_color_picker_consumers.mjs (M2). scripts/mutation-gate.mjs (2 новых мутанта, M1+M2), scripts/smoke-links.mjs (реестр выбора смоков). Оба changelog, docs/TESTING.md, docs/specs/README.md, пересобранные dist/**/custom_components/houseplan/frontend/**, обновлённый docs/images/screenshots.json (только fingerprint). Backend, геометрия, config/storage не затронуты.

Как проверялось

Зелёного Validate на 6e0e1f8c нет (gh run view 34032363066 — completed/failure, единственный красный job — Golden-кадры против принятых эталонов; Фронтенд, Предполётные проверки, оба перф/смок-джоба и все 3 шарда браузерных смоков — success). Прогнал сам:

Гейт Команда Результат
Typecheck npx tsc --noEmit чисто, без вывода
Unit-тесты npm test 2077 tests, pass 2076, fail 0, skipped 1 — совпадает с заявленным
Build + sync npm run build && npm run bundle:sync dist собран; cmp dist/houseplan-card.js custom_components/houseplan/frontend/houseplan-card.js — идентичны; git status --short пуст после сборки
Docs-гейт node scripts/check-docs.mjs Documentation checks passed (7 files, 12 external links) — обязателен, diff трогает src/**
Новый any node scripts/no-new-any.mjs --base origin/dev --head HEAD «Новых any нет» (59 добавленных строк в 2 файлах — те же, что в r1: делта не добавила новых строк в .ts)
Process-gate node scripts/process-gate.mjs «гейт пройден, предупреждений 0» (9 коммитов в диапазоне)
Выбор смоков node scripts/smoke-select.mjs --base origin/dev --head HEAD Прямое совпадение (1): smoke_color_picker_consumers.mjs (← _surface, новый файл появился в диффе). Зарегистрированная связь (2): smoke_color_picker.mjs, smoke_help_affordance.mjs. Слабая связь (15, stopPropagation) — те же имена, что и в r1 (smoke_cover_*, smoke_decor, smoke_edit_walk, smoke_editor_gestures, smoke_furniture, smoke_hide_layers, smoke_inert_openings, smoke_modes, smoke_room_cards, smoke_space_settings, smoke_tap_ctx, smoke_tap_run, smoke_toggle_confirmation, smoke_value_face_source) — просмотрел список повторно, все про другие поверхности; не гонял
Целевой смок 1 node demo/smoke_color_picker.mjs OK, все 21 поле true, включая repeatedConfirmCannotBypassInvalidHex: true
Целевой смок 2 (новый) node demo/smoke_color_picker_consumers.mjs OK, все 13 полей true, включая generalConfirmDoesNotClickThrough: true
Целевой смок 3 node demo/smoke_help_affordance.mjs OK, все поля true, включая fallbackPickerHasConfirm/fallbackPickerConfirmCloses
Bundle budget npm run bundle:budget initial View 299595 B gzip, потолок 300500±2000 — в бюджете; запас 1471 Б — тот же пред-существующий долг #367, не новый
Golden (advisory) npm run golden:verify 15 different из 150 сцен: 5 названных в ТЗ color-popover сцен (ожидаемо, см. AC1) + 10 isometric-*, не относящихся к этому диффу (см. отдельный раздел ниже); 135 passed
Мутация 1 (M1) node scripts/mutation-gate.mjs --id=color-picker-invalid-confirm-latch-removed патч снимает if (this._hexNeedsValidInput) {...} в _commitHex; guard — smoke_color_picker.mjs → «тест покраснел, как обязан», «поймано 1 из 1»
Мутация 2 (M2) node scripts/mutation-gate.mjs --id=color-picker-confirm-click-through патч снимает event.stopPropagation() из _confirm; guard — новый smoke_color_picker_consumers.mjs → «тест покраснел, как обязан», «поймано 1 из 1»
Применимость всех мутантов node scripts/mutation-gate.mjs --check все патчи ложатся на текущий код, включая оба новых

Не гонял и почему (не изменилось с r1, диф их не задевает):

  • python -m pytest tests_backend — diff не трогает custom_components/**/*.py.
  • npm run invariants / model-invariants.mjs — diff не меняет рёбра комнат, толщину, layout, marker.space, open_spans.
  • Performance-профили — не названы в AC; §15 ТЗ обоснованно утверждает отсутствие влияния (один статический button, один click-handler в уже открытом lazy-picker); perf-smoke job в CI зелёный.
  • Полная матрица demo/smoke_*.mjs и полный scripts/mutation-gate.mjs (226 смоков, десятки мутантов) — предрелизные гейты (PROCESS §8), не гейт код-ревью на задаче такого объёма; прогнаны только целевые/прямые + два новых мутанта.
  • npm run golden:accept — не моя роль на этом этапе.
  • «Одно число — один источник»: диф не добавляет и не меняет пользовательскую числовую величину (кнопка не показывает значение), правило неприменимо; test/single-source-numbers.test.mjs зелёный в составе npm test.

Golden: 10 isometric-* diff — не относится к #476

golden:verify на этой ветке даёт 15 different. Пять совпадают с ТЗ (§13.4: decor-color-popover-mobile-ru, decor-color-popover-desktop-en, general-color-popover-desktop-en, device-ripple-color-popover-mobile-ru, space-room-color-popover-desktop-ru) — просмотрел actual/+diff/ глазами, разница ровно в кнопке и вертикальном сдвиге содержимого, цвет/геометрия/тема не поехали. Остальные десять — все сцены isometric-* (isometric-geometry-view-{dark,light}, isometric-live-layers-dark, isometric-touch-kiosk-dark, isometric-large-warm-remount-dark, isometric-stage3-overlays-{light,dark}, isometric-stage3-openings-dark, isometric-stage3-forced-colors-dark, isometric-stage3-no-filter-dark) — их не должно быть, диф этой задачи не трогает ни один src/*isometric* файл.

Проверил происхождение: поднял временный git worktree add /tmp/dev-check origin/dev --detach (чистый fc5973c2, без единого коммита #476), собрал бандл (npm run bundle:sync) и прогнал golden:verify там же — те же ровно 10 сцен isometric-* выходят different, 0 отношения к color-picker. Chromium совпадает с эталоном побайтово по версии (151.0.7922.34 — demo/golden/baselines/baselines-index.json vs установленный движок), так что это не известный кейс расхождения рендеринга по окружению. Причина видна по истории dev: be5eeb41 fix: remove isometric overlay plates (issue #471) поменял рендер, но последний коммит, принимающий golden-эталоны, — de215578 test: accept isometric stage 3 golden baselines, до be5eeb41; после него в dev нет коммита с принятыми isometric-эталонами. Это преднесённый долг dev от #471, полностью независимый от этой ветки — красный CI job Golden-кадры против принятых эталонов на 6e0e1f8c красится и по этой причине тоже, но её причина не в #476. Golden — предрелизный, не код-ревью гейт (PROCESS §8); не завожу отдельный issue: не моя находка по существу (баг чужой, уже смердженной задачи #471, чинится обычным циклом приёмки эталонов перед бетой, а не патчем из этой ветки), и я не уверен, что это дефект, а не намеренно отложенное до релизного гейта принятие — распознавать и чинить чужой merged-код здесь означало бы расширять скоуп этой задачи.

Закрытие раунда r1

Находка r1 Чем закрыта Где видно
M1 — AC4-защита (_hexNeedsValidInput) доказана только смоком, без постоянного мутанта в scripts/mutation-gate.mjs Зарегистрирован мутант color-picker-invalid-confirm-latch-removed (guard demo/smoke_color_picker.mjs) scripts/mutation-gate.mjs (коммит 6e0e1f8c); лично прогнал --id=color-picker-invalid-confirm-latch-removed → «тест покраснел, как обязан»
M2 — AC3-защита (stopPropagation() в _confirm) не доказана тестом, способным упасть — единственный проверенный потребитель гасил клик выше по DOM независимо от мутации Добавлен demo/smoke_color_picker_consumers.mjs, сценарий generalConfirmDoesNotClickThrough на «lightOn» (general settings, потребитель без внешнего гасителя); зарегистрирован мутант color-picker-confirm-click-through (guard — новый смок) demo/smoke_color_picker_consumers.mjs:49-63, scripts/mutation-gate.mjs (коммит 6e0e1f8c); лично прогнал --id=color-picker-confirm-click-through → «тест покраснел, как обязан»

Оба закрытия проверены исполнением мутанта, не заявлением автора: патч применяется к _commitHex/_confirm, гоняется именно названный guard, тест красится, патч откатывается, git status --short пуст.

Унаследовано из r1 (без повторной проверки)

Документ: docs/reviews/CODE-REVIEW-476-r1.md, материал — дерево 94485fdc0eca504692dd2776c7f3c86518a8dfb9 (SHA-анкеры мертвы после второго ребейза, см. раздел выше; сам r1-документ пережил ребейз бит-в-бит, коммит baea0fcf).

Основание наследования — не доверие, а факт: src/hp-color-opacity.ts не менялся ни одним коммитом между 82f65acd (реализация, предшествующая r1) и HEAD; я лично перечитал этот файл целиком в текущем HEAD (строки 480–740) и построчно сверил с описанием r1 (_hexNeedsValidInput семантика, _confirm/_commitHex/_hexInput взаимодействие, DOM-порядок кнопки, CSS-контракт) — расхождений нет.

Наследуется без повторной проверки:

  • AC1 (видимая кнопка/геометрия) — unit + 5 golden-сцен, разница только в кнопке (я сам пересмотрел эти же 5 сцен в этом заходе, см. таблицу гейтов — согласуется с r1);
  • AC2 (live parity, без дублирующего события) — мутация r1 №2 (снятие условия дедупликации в _commitHex) остаётся в силе, код не менялся;
  • AC3 (успешное завершение, кроме click-through-защиты — она пересмотрена заново выше) — закрытие/фокус-часть, код не менялся;
  • AC5 (прежние close paths) — Escape/outside/trigger, код нетронут;
  • AC6 (i18n/a11y) — 4 языка, DOM-порядок = Tab-порядок;
  • AC7 (touch/fallback/lifecycle, кроме нового generalConfirmDoesNotClickThrough-сценария) — Popover/fallback паритет;
  • AC8 (совместимость/бюджеты) — нет новых config/storage/backend полей;
  • Low L1 (стилевая склейка houseplan-card.ts:5932), L2 (keyboard activation только через .click()), L3 (outside pointer/повторный trigger не покрыты автотестом, пред-существующий пробел) — сняты r1 с записью, делта их не касается, повторно не пересматривал.

Находки

Нет. Обе Medium-находки r1 закрыты и лично перепроверены исполнением (см. «Закрытие раунда r1»). Новых находок дельта (M1/M2-фикс + чистый пересбор dist/** при ребейзе) не внесла — единственный файл продуктовой логики (src/hp-color-opacity.ts) не менялся со времени r1.

High: 0. Medium в скоупе: 0. Medium вне скоупа: 0 (десять isometric-* golden-diff — не находка этой задачи, см. раздел выше; issue не завожу).

AC — таблица доказательств (r2, полная)

AC Чем доказан Чем краснеет Статус
AC1 видимая кнопка/геометрия unit + golden:verify (5 сцен, diff — только кнопка) не защитный AC, §2.7 не требует столбца унаследовано из r1, перепроверено визуально в этом заходе
AC2 live parity, без дублирующего события smoke confirmClosesWithoutDuplicateOrClickThrough, validCorrectionAllowsConfirm мутация r1 №2 (не перезапускал — код не менялся) унаследовано из r1
AC3 успешное завершение / без click-through smoke confirmClosesWithoutDuplicateOrClickThrough + новый generalConfirmDoesNotClickThrough мутация color-picker-confirm-click-through — лично прогнал в этом заходе, красное перепроверено в r2 (M2 закрыта)
AC4 невалидный HEX / защита от обхода unit + smoke invalidHex*, repeatedConfirmCannotBypassInvalidHex мутация color-picker-invalid-confirm-latch-removed — лично прогнал в этом заходе, красное перепроверено в r2 (M1 закрыта)
AC5 прежние close paths smoke escapeClosesFirstAndRefocuses; outside/trigger — проверено чтением (L3, пред-существующий пробел) код не тронут унаследовано из r1
AC6 i18n/a11y unit (i18n.test.mjs, color-picker.test.mjs) + smoke englishLabels/cardLanguageOwnsCopy не защитный AC унаследовано из r1
AC7 touch/fallback/lifecycle smoke confirmIsFullWidthTouchTarget, smoke_help_affordance.mjs (fallbackPickerHasConfirm/fallbackPickerConfirmCloses) + новый consumer-смок не мутировал отдельно (оба пути реально прогнаны) унаследовано + расширено новым смоком
AC8 совместимость/release/бюджеты no-new-any, check-docs, bundle:budget, оба changelog в 82f65acd не защитный AC унаследовано из r1

Что проверено и корректно

  • src/hp-color-opacity.ts идентичен коду, разобранному в r1: единственный коммит диапазона, трогающий файл (82f65acd), лежит до r1-ревью; перечитал файл заново и подтвердил соответствие описанию.
  • Обе Medium-находки r1 закрыты воспроизводимыми мутантами, лично прогнанными до и после патча: color-picker-invalid-confirm-latch-removed и color-picker-confirm-click-through оба «покраснели, как обязаны».
  • demo/smoke_color_picker_consumers.mjs тестирует click-through защиту на потребителе (general settings/lightOn) без внешнего гасителя событий — устраняет ровно ту слабость, которую нашёл r1 (там decor-tool consumer гасил клик независимо от продуктовой защиты).
  • Дельта после второго ребейза ограничена сгенерированными dist/**/custom_components/houseplan/frontend/** и docs/images/screenshots.json (fingerprint) — подтверждено: git log --oneline origin/dev..HEAD -- src/hp-color-opacity.ts даёт один коммит, предшествующий обоим ребейзам.
  • dist/custom_components/houseplan/frontend синхронны с исходником (cmp идентичны, git status --short пуст после пересборки).
  • Process-gate, трейлеры (Issue: #476 на всех 9 коммитах, User-Visible: yes только на 82f65acd с обоими changelog в этом же коммите) — корректны.
  • 10 golden-diff isometric-* подтверждённо не относятся к этому диффу: воспроизведены на чистом origin/dev в отдельном worktree без единого коммита #476.

Чего не проверял

  • pytest tests_backend, npm run invariants, performance-профили, полная матрица demo/smoke_*.mjs, полный scripts/mutation-gate.mjs (кроме двух целевых), golden:accept — см. обоснование в таблице гейтов.
  • 15 «слабых» смоков по stopPropagation — просмотрел список имён повторно (не изменился с r1), все про другие поверхности, не про hp-color-opacity.
  • Не расследовал глубже происхождение golden-долга #471 сверх того, что нужно, чтобы отделить его от этой задачи (не моя роль — задача другого issue).

Вывод

High: 0. Medium в скоупе: 0. Medium вне скоупа: 0. Verdict: зелёный.


Материал раунда

  • Ветка: issue/476-color-picker-ok, HEAD 6e0e1f8ca24b10a87569f9fec07810e08b7fa71c.
  • origin/dev = fc5973c2239a2d25485b1acc955d0b0da620370c, совпадает с git merge-base origin/dev HEAD — ветка полностью приведена, конфликтов нет.
  • Диапазон: git log --oneline origin/dev..HEAD (9 коммитов) / git diff origin/dev...HEAD (56 файлов).
  • Предыдущий документ: docs/reviews/CODE-REVIEW-476-r1.md (коммит baea0fcfedfacd0393a750750279ce4f869dd5b9), материал того захода — дерево 94485fdc0eca504692dd2776c7f3c86518a8dfb9 (SHA-анкеры 34a6b276473c/b5a1001e мертвы после второго ребейза, объяснено выше).

Материал раунда

  • Ветка: issue/476-color-picker-ok, коммит 6e0e1f8ca24b — ребейз его осиротит, и это нормально: ниже якоря, которые ребейз не меняет.
  • Дерево материала: 2e8d8ce4b2320c3d28cc4e815b1c45da973a797e
    git log --all --format='%H %T' | grep 2e8d8ce4b232
    
  • ТЗ docs/specs/476-color-picker-ok.md, блоб d21066d56e69c35fe7d0b40d9f965968dac9f803
    git log --all --find-object=d21066d56e69c35fe7d0b40d9f965968dac9f803 -- docs/specs/476-color-picker-ok.md