Files
houseplan-card/docs/reviews/CODE-REVIEW-594-r1.md
T
2026-09-19 11:08:04 +03:00

26 KiB
Raw Blame History

CODE-REVIEW-594-r1

Issue: #594 (шаг 1 эпика #591) · Ветка issue/594-form-kit-room · Материал: c54a7770d92469916dedecac7d4595c55bea0cbe (родитель 073c45b0) · Заход r1 · блокирующих циклов до этого раунда: 0/4

Скоуп

Диапазон origin/dev...HEAD (073c45b0..c54a7770), 2 коммита:

  • bd0cf829 feat: общий набор контролов формы и диалог комнаты по нему — User-Visible: yes
  • c54a7770 refactor: типизировать ключи заливки вместо any — User-Visible: no

Файлы вне генерируемого (класс D) и трейлеры проверены командой git diff origin/dev...HEAD и git log --format=full. Классы: A — src/editors/form-kit.ts (новый), src/styles/form-kit.styles.ts (новый), src/editors/room-settings-dialog.ts, src/houseplan-editor-runtime.ts, src/houseplan-card.ts, src/i18n/{ru,en,fr,de}.json; B — demo/smoke_room_settings.mjs, scripts/mutation-registry.mjs, test/*.test.mjs; C — docs/CHANGELOG*.md, docs/USER-GUIDE*.md. Всё укладывается в скоуп ТЗ (## ТЗ → «Скоуп / не-скоуп»); src/houseplan-card.ts теряет только тонкую обёртку _renderRoomSource, прямое следствие переноса, а не расширение скоупа. Оба коммита несут Issue: #594 и по одному User-Visible:; bd0cf829 (yes) правит docs/CHANGELOG.md и docs/CHANGELOG.ru.md в том же коммите — трейлеры и §11 PROCESS соблюдены.

ТЗ прошло ревью зелёным на r3 (SPEC-REVIEW-594-r3.md, High: 0, Medium: 0), обе Low-находки закрыты, i18n-таблица полна. Материал кода не редактировал тело issue после этого — вопросов к материалу ТЗ нет.

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

Инструкция разрешила не перегонять typecheck/test/build — Validate на c54a7770 зелёный (https://github.com/Matysh/houseplan-card/actions/runs/35394685119). Проверено отдельно, что это действительно материал ревью: headSha прогона == c54a7770d92469916dedecac7d4595c55bea0cbe. Тем не менее часть работы требовала свежего локального дерева (браузерные смоки, golden, docs), поэтому npm run build/npm run bundle:sync выполнялись как побочный эффект — без отклонений от закоммиченного бандла (git status после сборки — чисто, дерево дистрибутива побайтово совпало).

Гейт Результат Почему
npx tsc --noEmit, npm run build не гонял отдельно — уже в Validate c54a7770; но npm run build/bundle:sync выполнялись для смоков/golden и прошли чисто (tsc --noEmit && rollup) дешёвый гейт подтверждён CI
npm test (весь набор) не гонял целиком — подтверждён Validate (Фронтенд: типы, юниты, мутанты, синхрон бандла = success); прогнал вручную целевой поднабор см. ниже
node --test test/i18n.test.mjs test/i18n-dead-keys.test.mjs test/form-kit.test.mjs test/styles-split.test.mjs 43/43 pass ключевые тесты этой задачи, включая замороженную фикстуру AC11
node scripts/no-new-any.mjs --base origin/dev --head HEAD «новых any нет» подтверждает решение коммита c54a7770
npm run bundle:budget initial View 291872 Б (потолок 292400±2000), запас 528 Б; сверено с origin/dev (291356 Б, запас 1044 Б) — рост ровно +516 Б, как заявлено AC8
node scripts/mutation-gate.mjs --id=form-kit-writes-to-a-neighbour-key поймано 1 из 1 AC2 защитный столбец «чем краснеет»
node scripts/mutation-gate.mjs --id=form-kit-segment-drops-radio-semantics поймано 1 из 1 AC5/К7 защитный столбец
node demo/smoke_room_settings.mjs все ключи out truthy, OK AC1/AC2/AC5/AC6, включая новые nameWritesOnlyItsOwnKey, scaleWritesOnlyItsOwnKey, humiditySourceWritesOnlyItsOwnKey, everyGroupHasHelp, sourceSegment*
node demo/smoke_color_picker_consumers.mjs, smoke_room_temperature_thresholds.mjs, smoke_help_affordance.mjs, smoke_summary_panel.mjs, smoke_summary_panel_polish.mjs, smoke_font_scales.mjs, smoke_editor_tabs.mjs все OK AC3, AC4, AC6 (доп.), AC9, регресс на соседей из «прямого совпадения» smoke-select
node scripts/smoke-select.mjs --base origin/dev --head HEAD 19 «прямых совпадений» (все выше плюс 12 геометрических/декоровых смоков, не относящихся к форме — не гонял: символы _curSpaceCfg/_nameSel совпадают случайно, диалог настроек комнаты их не трогает по логике письма) + 31 «слабых связей» по _curSpaceCfg (не гонял — общий контекстный символ, форма не меняет геометрию/decor) выбор смоков соразмерно диффу
npm run golden:verify (полный линуксовый набор, Chromium 151.0.7922.34 — совпадает с индексом эталонов) два расхождения сверх объявленных — см. находку M1 AC7
npm run docs:accept -- --identical (диагностика, изменения отменены) все 11 кадров документации побайтово совпали; принимать нечего, кроме отпечатка исходников AC/Release-артефакты, см. находку M2
python -m pytest tests_backend не гонял — диф не касается custom_components/**/*.py не применимо
перф-профили не гонял — диф не касается src/iso-*, src/live-*, src/render-*, houseplan-render-lifecycle.ts, houseplan-card.ts рендер-путей (только удаление тонкой обёртки); лист набора — в ленивом графе, новых проходов по данным нет (AC8 подтверждает размер, не время) вне AC, вне названных путей
git worktree на origin/dev (073c45b0) — контрольный bundle:budget и golden:verify воспроизвёл то же самое: заголовок headroom-предупреждения и «different» device-icon-state-table-{light,dark} уже были на dev до этой задачи атрибуция находок ниже

Не гонял: полную матрицу смоков (250 файлов) — задача не задевает всё; pytest; perf-профили (не применимо по путям). Обоснование выбора приведено в таблице.

Находки

Medium (в скоупе задачи) — M1: AC7 не закрыт до конца — эталоны не приняты

npm run golden:verify на полном линуксовом артефакте (Chromium 151.0.7922.34, совпадает с baselineManifest.chromium в эталонах — то самое несоответствие 152/151, которое автор не смог обойти в песочнице, здесь не воспроизводится) даёт:

different  device-icon-state-table-light
different  device-icon-state-table-dark
different  room-temperature-dialog-desktop-en
different  room-temperature-dialog-mobile-ru

Две последние — ровно объявленные AC7 сцены, и визуально они корректны (см. artifacts/golden/actual/room-temperature-dialog-{desktop-en,mobile-ru}.png — карточки «Basics/Fill/Sensor sources», «Основное/Заливка», сегмент источника, значения и порядок полей совпадают с ТЗ). Две первые (device-icon-state-table-*) я атрибутировал отдельно: контрольный прогон golden:verify на чистом origin/dev (073c45b0, до этой задачи) даёт то же самое расхождение по этим двум сценам — это уже существующий, не связанный с #594 дефект (сцена добавлена под #588; регрессия чужая, трогать её в этой задаче нельзя, а заводить отдельный issue — не моя роль корректировать чужой скоуп находкой этого ревью, если только автор сам не поднимет вопрос; это pre-existing и не входит в блокирующий счёт).

Проблема в другом: AC7 требует не только «диф ограничен объявленными сценами» (это так), но и того, что эталоны для этих двух сцен пересняты и приняты («Release-артефакты»: «эталоны двух объявленных сцен»). В диффе git diff origin/dev...HEAD --stat нет ни одного изменения demo/golden/baselines/** — новые кадры не приняты, коммит не несёт Release:/Baseline-Reviewed:. Если эту ветку слить как есть, room-temperature-dialog-{desktop-en,mobile-ru} останутся красными на первом же тяжёлом прогоне (кандидат беты, nightly) — притом что расхождение полностью предсказано и ожидаемо, не «дефект, который не мог проявиться раньше» (§11.4 сюда не подходит).

Чем краснеет: npm run golden:verify на полном Linux-артефакте — уже красное на этом материале. Правка: принять новые эталоны npm run golden:accept -- --reviewed <ссылка на зелёный тяжёлый Validate/Docs-прогон> с коммитом, несущим Release:/Baseline-Reviewed: (§10.1, правило 13/PROCESS.md), и запушить перед повторным заходом на код-ревью.

Medium (в скоупе задачи) — M2: отпечаток скриншотов документации не обновлён

node scripts/check-docs.mjs красит: ERROR screenshot source fingerprint is stale. Диф трогает src/**, поэтому по правилу «выбирать тут нечего» (PROCESS.md §8, прецеденты #230/#234/#237) это ожидаемо и обязательно к починке до слияния — иначе dev получает красный job docs до следующей задачи, как уже дважды случалось. Проверил, что дело именно в отпечатке, а не в реальном визуальном расхождении: npm run docs:accept -- --identical (прогнал как диагностику, изменения в рабочей копии затем отменил) — «Все 11 кадров попиксельно совпали с закоммиченными: принят только отпечаток исходников». Диалог настроек комнаты действительно не открывает ни один из 10 сценариев demo/docs (ТЗ это верно называет), поэтому фикс тривиален и не требует пересъёмки на CI.

Чем краснеет: node scripts/check-docs.mjs — красное сейчас. Правка: npm run docs:accept -- --identical и закоммитить обновлённый docs/images/screenshots.json (класс C, не требует отдельного issue — часть DoD этой задачи).

Medium (в скоупе задачи) — M3: AC1 доказан у́же, чем заявлено

ТЗ обещает для AC1 «новый unit test/form-kit.test.mjs — таблица «контрол → ключ → записанное значение» на фейковом хосте» для восьми полей: имя, область, режим заливки, цвет, пороги, два источника, два масштаба. По факту test/form-kit.test.mjs содержит три теста другого назначения (дословность CSS-фикстуры панели — AC11, параметризация имён генератора, радиосемантика сегмента — AC5/К7); таблицы «控трол → ключ» там нет вовсе. Контракт «пишет только своё поле» вместо этого проверяет расширенный demo/smoke_room_settings.mjs, но только для 3 из 8 полей: name, nameScale, humSrc (nameWritesOnlyItsOwnKey, scaleWritesOnlyItsOwnKey, humiditySourceWritesOnlyItsOwnKey). Цвет и пороги закрыты отдельно неизменными smoke_color_picker_consumers.mjs/smoke_room_temperature_thresholds.mjs (AC3/AC4 — это честно). Но область (_areaSel) и режим заливки (_roomFill, включая только что перетипизированный FILL_CHOICES) не имеют исполнимого oracle вообще — ни unit, ни smoke не кликает по <select class="areasel"> или по радио заливки и не проверяет, что меняется ровно ожидаемое поле. tempSrc тоже не проверен напрямую (только humSrc, хотя обработчик один и тот же для обоих kind, риск частично перекрыт мутантом form-kit-writes-to-a-neighbour-key, который ловит именно перепутывание temp/hum).

Прочитал код (не исполнением) для _areaSel и FILL_CHOICES: логика письма побайтово перенесена из прежней версии (this.host._areaSel = ..., this.host._roomFill = value; if (value !== 'custom') this.host._roomCustomFill = null;), только тип пары [value, key] заменён на явный RoomFillChoice; риск регрессии от этого рефакторинга оцениваю как низкий, но это моё суждение по чтению, а не доказанный автотестом факт, а соразмерный исполнимый oracle здесь дёшев — по образцу уже добавленных в тот же смок трёх проверок.

Чем краснеет: сейчас ничем — пробел, а не красный тест. Правка: либо добавить в demo/smoke_room_settings.mjs аналогичные areaWritesOnlyItsOwnKey/fillWritesOnlyItsOwnKey (пара строк по образцу уже написанных), либо явно закрыть AC1 записью «проверено чтением, не исполнением» для этих двух полей в хендоффе — сейчас в хендоффе об этом сужении не сказано ни слова, и заявлено «AC1 | три теста» так, как будто покрытие полное.

Low — не блокирует

  • Автор в хендоффе не упомянул npm run docs:accept вовсе (ни как сделанное, ни как «не сделано») — при том что это явно требование Release-артефактов ТЗ. Раздел «Не прогонял» перечисляет golden:verify, полный HA-харнесс, перф-профили и полную матрицу смоков, но не docs. Снимаю как Low (входит в M2 по существу), фиксирую отдельно только как процессное наблюдение: список «не сделано» в хендоффе не был полным.

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

  • К1 (запись не меняется). Прочитан весь новый renderRoomSettingsDialog (src/editors/room-settings-dialog.ts) построчно против версии на origin/dev: все восемь ключей черновика (_nameSel, _areaSel, _roomFill/_roomCustomFill, _roomTempMin/_roomTempMax через неизменный roomTemperatureControls, _roomTempSrc/_roomHumSrc через вынесенный renderRoomSource, _roomNameScale/_roomLabelScale) читают и пишут те же поля хоста, что и до рефакторинга — код по существу перенесён, а не переписан. Новых ключей конфигурации нет.
  • К2 (условия сохранения). canSaveNew и ?disabled=${!this.host._nameSel.trim() || !tempValid} — байт в байт как в origin/dev; «активно только при изменениях» в диффе не появилось.
  • К3 (цвет). hp-color-opacity не подменён, событие hp-color-opacity-change и атомарная запись цвета+прозрачности — без изменений; colorRow() — чисто раскладочная обёртка. smoke_color_picker_consumers.mjs (без правок) зелёный.
  • К4 (пояснения). Все четыре заголовка групп используют this._help('room.group_*.help'|'room.sizes_section.help'), чья сигнатура (houseplan-editor-runtime.ts:1263) принимает только ${string}.help и сама достраивает .aria — паттерн hp-help соблюдён, подтверждено smoke_help_affordance.mjs (без правок) и новой проверкой everyGroupHasHelp в smoke_room_settings.mjs.
  • К5 (набор общий, не карточка-специфичный). form-kit.ts не содержит ни одной ссылки на _room*; все операции параметризованы (FormCardOptions, FormRowOptions, SegmentedOptions<T>, ColorRowOptions). Стили — чистый генератор строки (formKitCss(options, flags)), без побочных эффектов.
  • К6 (панель не тронута). git diff --stat origin/dev...HEAD не содержит ни одного src/summary-panel-*; smoke_summary_panel.mjs и smoke_summary_panel_polish.mjs (без правок) зелёные.
  • К7 (доступность сегмента). segmented() рендерит настоящую role="radiogroup" с <input type="radio" name=...>; test/form-kit.test.mjs статически проверяет разметку, demo/smoke_room_settings.mjs — реальные размеры цели (≥44px) и фокус в браузере; мутация type="radio"→"checkbox" ловится (поймано 1 из 1).
  • AC8 (бюджет). Число совпадает с заявленным (+516 Б до 291872, потолок 292400±2000); лист набора действительно не входит в cardStyles (test/styles-split.test.mjs, «issue 266 the aggregator is exactly the five surface files» — не тронут) и вносится в теневой корень отдельно (ensureFormKitStyles, adoptedStyleSheets/фолбэк на <style>); новый тест «adds selectors instead of overriding» подтверждает отсутствие коллизий с существующими правилами каскада.
  • AC11. Тест сверяет фикстуру с живым файлом src/summary-panel-editor-style.ts (а не с самим собой), и это осмысленно устраняет риск «фикстура тихо разошлась с панелью» — прогнал отдельно, зелёный.
  • no-new-any. Причина рефакторинга во втором коммите (типизация FILL_CHOICES вместо _t(k as any)) реальна и устраняет any, а не прячет его; гейт подтверждён локально.
  • i18n. Все 4 локали (ru/en/fr/de) содержат все 11 новых ключей и не содержат удалённого room.settings_section; test/i18n-dead-keys.test.mjs обновлён с явным обоснованием числа (20→24); test/i18n.test.mjs получил осознанный тест на «единый хозяин help у карточек набора» вместо ослабления старой проверки.
  • Changelog/USER-GUIDE. Оба changelog и оба User-Guide правлены в коммите bd0cf829 (User-Visible: yes), терминология («Основное», «Заливка», «Источники», «Размеры шрифтов») совпадает с i18n-ключами и с фактическим текстом на золотых кадрах.
  • Классы риска §2.6. async — не применимо (нет асинхронных операций). Данные/права — запись в те же ключи черновика, прав не касается (см. К1). Геометрия — не применимо. Визуал — главный риск, покрыт AC7 (см. M1: диф ограничен объявленными сценами, но эталоны не приняты). Объём/perf — AC8 подтверждён числом. Host/input — клавиатура и цели нажатия подтверждены смоком в браузере.

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

  • Полную матрицу из 250 golden-сцен и всех 250/~19 «слабо связанных» смоков — диф не задевает геометрию, decor, junction-логику; выборка по smoke-select.mjs и прямому совпадению признана достаточной.
  • pytest tests_backend — диф не касается custom_components/**/*.py.
  • Perf-профили — диф не в путях, которые их запускают, и лист стилей — в ленивом графе без новых проходов по данным.
  • Полный HA-харнесс (WSL) — не требуется этим диффом, автор тоже не прогонял.
  • Причину существующего расхождения device-icon-state-table-{light,dark} не расследовал за пределами атрибуции «не введено этой задачей» (подтверждено идентичным результатом на origin/dev до #594) — это чужой, добавленный под #588 дефект, вне скоупа этого ревью.

Вердикт

Жёлтый. High: 0, Medium: 3 (все в скоупе задачи — M1 приёмка эталонов, M2 отпечаток скриншотов, M3 неполное покрытие AC1 для двух полей). Реализация сама по себе соответствует ТЗ построчно и по поведению (К1–К7, AC1–AC6, AC8, AC9, AC11 подтверждены исполнением или чтением с явной пометкой), но задача не может считаться завершённой, пока не закрыты M1/M2 (иначе dev получает предсказуемо красные golden/docs гейты — ровно тот сценарий, из-за которого #230/#234/#237 попали в процесс) и не уточнено покрытие M3.

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

  • SHA материала: c54a7770d92469916dedecac7d4595c55bea0cbe (branch issue/594-form-kit-room, родитель 073c45b0)
  • Дерево: рабочая копия совпадает с этим SHA на момент вывода вердикта (git rev-parse HEAD сверен непосредственно перед публикацией)
  • Диапазон: origin/dev...HEAD = 073c45b0..c54a7770

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

  • Ветка: issue/594-form-kit-room, коммит c54a7770d924 — ребейз его осиротит, и это нормально: ниже якоря, которые ребейз не меняет.
  • Дерево материала: 59dc67066a10f550047f3d33e6da0fb2a8ed5976
    git log --all --format='%H %T' | grep 59dc67066a10
    
  • Тело issue: 594c7771e678dae203db75069c75806da3eec9c1d9b155a3d7ba2236d338052a
  • Вердикт конвейера: yellow · High 0