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

127 lines
26 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-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`
---
<!-- material-anchors: сгенерировано конвейером (#414) -->
## Материал раунда
- Ветка: `issue/594-form-kit-room`, коммит `c54a7770d924` — ребейз его осиротит, и это нормально: ниже якоря, которые ребейз не меняет.
- Дерево материала: `59dc67066a10f550047f3d33e6da0fb2a8ed5976`
```
git log --all --format='%H %T' | grep 59dc67066a10
```
- Тело issue: `594c7771e678dae203db75069c75806da3eec9c1d9b155a3d7ba2236d338052a`
- Вердикт конвейера: `yellow` · High 0