docs: review document for #487
Проверка (CI) / Классификация изменённых файлов (push) Successful in 24s
Проверка (CI) / Предполётные проверки: документация, провенанс, процесс (push) Failing after 43s
Проверка (CI) / HACS: валидация репозитория (push) Failing after 32s
Проверка (CI) / Hassfest: манифест интеграции (push) Failing after 32s
Проверка (CI) / Переиспользование: это дерево уже проверено (push) Successful in 46s
Проверка (CI) / Мутанты по диффу (3/3): затронутые свидетели краснеют (push) Failing after 4m0s
Проверка (CI) / Фронтенд: типы, юниты, мутанты, синхрон бандла (push) Failing after 11m21s
Проверка (CI) / Смоки в браузере (шард 1 из 3) (push) Skipped
Проверка (CI) / Смоки в браузере (шард 2 из 3) (push) Skipped
Проверка (CI) / Смоки в браузере (шард 3 из 3) (push) Skipped
Проверка (CI) / Смоки: все шарды зелёные (push) Skipped
Проверка (CI) / Golden-кадры против принятых эталонов (push) Skipped
Проверка (CI) / Перф-смок: бюджет времени кадра (push) Skipped
Проверка (CI) / Мутанты по диффу (1/3): затронутые свидетели краснеют (push) Failing after 13m11s
Проверка (CI) / Бэкенд: pytest в Home Assistant (push) Failing after 10m1s
Проверка (CI) / Мутанты по диффу (2/3): затронутые свидетели краснеют (push) Failing after 18m36s

Issue: #487
User-Visible: no
This commit is contained in:
claude[bot]
2026-09-08 08:11:43 +00:00
parent 68462b81e0
commit 3a702b3124
+232
View File
@@ -0,0 +1,232 @@
# CODE-REVIEW-487-r1
- **Issue:** https://github.com/Matysh/houseplan-card/issues/487
- **Заход:** r1 (первый заход код-ревью; блокирующих циклов израсходовано 0/4)
- **Ветка:** `issue/487-room-temperature-thresholds`
- **SHA материала:** `68462b81e08b6f4c3f366703879199bf5e493374` (проверено `git rev-parse HEAD` непосредственно перед выводом)
- **ТЗ:** `docs/specs/487-room-temperature-thresholds.md` (ревью ТЗ — зелёное, SPEC-REVIEW-487-r1)
## Скоуп диффа
`git diff origin/dev...HEAD`, 78 файлов. Продуктовый код: `src/types.ts`,
`src/logic.ts` (`roomTempRangeOf`), `src/space-dialog.ts` (draft/writer
helpers), `src/room-temperature-controls.ts` (новый, UI блока), `src/houseplan-card.ts`
(Full View: state, resolver, дренаж диалога), `src/houseplan-editor-runtime.ts`
(диалог создания/редактирования комнаты, writer, кнопки Save), `src/space-render.ts`
(static card), `src/styles/dialogs.styles.ts`, i18n en/ru/de/fr, backend
`validation.py` (`_finite_number_or_none`, `ROOM_SCHEMA`), `import_export.py`
(`_ROOM_DISPLAY_FIELDS`). Плюс тесты (unit/backend/smoke/golden/mutation),
config-schema/registry, changelog RU+EN, USER-GUIDE RU+EN, CONFIG-COMPATIBILITY,
ARCHITECTURE, bundle-budget.
История ветки содержит один задокументированный инцидент до код-ревью: хендофф
`S6→S7` в 07:40 не смог перейти в ревью, потому что ветка не ребейзилась на
`dev` без конфликта (комментарий 07:41, конфликт только в `dist/**` и
`custom_components/houseplan/frontend/**` — сгенерированный класс D). Это не
цикл ревью: код никто не читал, вердикта не было. После `git rebase origin/dev`
и пересборки бандла (коммиты `e2751062`…`68462b81`) ветка вернулась в
`S7-code-review` в 07:58; именно эта версия и есть материал настоящего r1.
## Как проверялось
### Дешёвые гейты — сняты с зелёного Validate на точном SHA (не гонялись повторно)
Validate `68462b81` (https://github.com/Matysh/houseplan-card/actions/runs/34201690270) —
`success`; job «Фронтенд: типы, юниты, мутанты, синхрон бандла» зелёный, что
покрывает `npx tsc --noEmit`, `npm test`, `npm run build` + сверку бандла.
`docs`/`process-gate`/`hacs`/`hassfest` — зелёные там же.
На этом SHA тяжёлые джобы (`smoke`, `golden`, `performance_smoke`, `backend`)
в самом `68462b81` — `skipped` (обычный push, не release-candidate — §8). Но
`git diff 89deee0f..68462b81 --stat` показывает единственное отличие —
`docs/images/screenshots.json` (провенанс скриншотов документации, не код).
На `89deee0f` (родитель `68462b81`, идентичный функционально) прогон
https://github.com/Matysh/houseplan-card/actions/runs/34201299329 (`workflow_dispatch
full=true`) дал: `Бэкенд: pytest` → success, `Golden-кадры против принятых
эталонов` → success, `Смоки: все шарды зелёные` (1/3, 2/3, 3/3) → success,
`Мутанты по диффу` (1/3, 2/3, 3/3) → success, `Перф-смок` → success. Общий
конклюжен run был `failure` только из-за `Предполётные проверки: документация`
(ожидаемо — экран-провенанс устарел до докоммита `68462b81`, который его и
чинит). Поскольку единственная разница между `89deee0f` и `68462b81` —
docs-провенанс, результаты тяжёлых джобов на `89deee0f` переносятся на текущий
SHA без повторного прогона.
**Явно НЕ гонял:** `npm run gate:small` целиком, `npm run golden:capture`,
полный `npm test` локально (не нужно — см. выше), `python -m pytest
tests_backend -q` целиком (targeted-прогон уже зафиксирован в CI-джобе
«Бэкенд»).
### Что прогнал сам
- `node scripts/smoke-select.mjs --base origin/dev --head HEAD` — вывод см.
ниже, прямых совпадений 22, слабых связей (только `_roomDialog`) 3.
Диффу для этой задачи целиком покрывающий смок — новый
`smoke_room_temperature_thresholds.mjs`, уже зелёный в CI (шард 3/3, run
34201299329). Остальные 21 прямых совпадений (`_nameSel`, `_areaSel`,
`_roomDialog`, `_climate`, `_renderPlanHass`, `_markers`) относятся к
существующим, не тронутым этой задачей веткам поведения (стены, мердж/сплит,
автозакрытие) — изменения в этой задаче в `_saveRoom`/`_saveRoomEdit`/
`_openRoomEdit`/`_resetRoomDialogFields` строго аддитивны и активируются
только при непустом черновике температуры (проверено чтением — см. ниже),
поэтому дополнительный прогон всей группы не требуется; 3 слабые связи
(`_roomDialog` без иного пересечения) также не запускал — риск регрессии в
них не выше, чем в 21 пропущенном прямом совпадении, и по той же причине.
- `node scripts/mutation-gate.mjs --id=room-temperature-blank-becomes-zero` →
чистый прогон **ok**, мутант **ok: тест покраснел, как обязан**.
- `node scripts/mutation-gate.mjs --id=room-temperature-min-inherits-wrong-side`
→ чистый прогон **ok**, мутант **ok: тест покраснел, как обязан**.
- Ручная мутация вне реестра (для AC7, «чистый юнит», по правилу §2.7
достаточно прогона со снятой защитой, приведённого в документе): в
`src/space-render.ts` временно заменил `tempRange.min, tempRange.max,` на
`disp.tempMin, disp.tempMax,` (тот самый регресс, который описывает AC7 —
«один из renderer paths продолжает передавать disp.tempMin/max напрямую») и
прогнал `node --test test/room-temperature-renderers.test.mjs`: тест
**упал** (`assert.doesNotMatch` сработал). Патч отменён сразу после
прогона, дерево чистое (`git status` — clean).
- Прочитал (не исполнял отдельно, потому что уже покрыто CI выше): весь
продуктовый дифф построчно — `src/logic.ts`, `src/space-dialog.ts`,
`src/room-temperature-controls.ts`, `src/houseplan-card.ts` (включая
`_resolvedRoomFills`, `_roomTemp`/`_roomHum`, обработчики диалога),
`src/houseplan-editor-runtime.ts` (`_saveRoom`, `_saveRoomEdit`,
`_roomSettingsFromDialog`, `_openRoomEdit`, `_resetRoomDialogFields`,
`_renderRoomDialog`), `src/space-render.ts`, backend `validation.py`/
`import_export.py`, все i18n правки, `scripts/config-schema.json`,
`scripts/config-field-registry.mjs`, `scripts/bundle-budget.mjs` (объяснение
поднятого потолка 290 500→291 500 с измеренным фактом 290 937 Б).
- Проверил, что провенанс golden-эталонов реален: коммит `89deee0f` несёт
`Release: v1.73.0-beta.6` и `Baseline-Reviewed: .../runs/34198109380`. Этот
прогон в целом `cancelled`, а его job «Golden-кадры против принятых
эталонов» — `failure`. Проверил семантику: `goldenRunFailed()` в
`demo/golden/policy.mjs` считает verify-прогон красным, если **любой**
сценарий не `passed` — а для двух совершенно новых сцен закономерно нет
эталона (`missing-baseline` ≠ `passed`), то есть красный статус этого job
ожидаем именно потому, что сцены новые, а не потому, что кадры неверны.
Подтверждение, что принятые эталоны корректны: `89deee0f` сам по себе (уже
содержащий новые эталоны) даёт `Golden-кадры против принятых эталонов` →
**success** в run 34201299329. Ложной тревоги не было, но стоило проверить
явно — процесс запрещает «зелёный ради зелёного» (правило 13), поэтому
ссылка на красный/отменённый прогон заслуживала отдельной проверки, а не
доверия на слово.
### smoke-select — вывод инструмента
```
Матрица: 231 смоков · порог «широкого» символа: больше 46 смоков
Прямое совпадение (22): smoke_room_settings, smoke_editor_tabs,
smoke_merge_split, smoke_room_autoclose, smoke_draw_wall_thickness,
smoke_feedback_v2, smoke_island_rooms, smoke_junction_limits,
smoke_plan_drawing_repairs, smoke_room_temperature_thresholds (← _roomTempMax,
_roomTempMin — новый смок, целевой),
smoke_unified_wall_tool, smoke_v8_draft_write, smoke_wall_face_overlap,
smoke_wall_thickness_transition, smoke_zero_divider_taper, smoke_binding_picker,
smoke_climate_once, smoke_climate_temp, smoke_linked_virtual_light,
smoke_registryless_opening, smoke_render_invalidation, smoke_room_climate_placement
Слабая связь (3, только по _roomDialog): smoke_plan_snap_overlay,
smoke_split_nonsnap, smoke_split_polyline
```
## Находки
Нет находок уровня High или Medium.
**Low (не блокирует, зафиксировано без правки):**
1. `src/houseplan-card.ts`, метод `_roomTemp`/`_roomHum` (около строки 12559) —
попутный рефакторинг, не требуемый задачей: две задокументированные
функции с раздельными JSDoc-комментариями схлопнуты в однострочный
тернарник без комментариев. Поведение идентично (сравнил построчно: было
`if (src) return sourceValue(...); ...; return key ? ... : null;`, стало
`src ? sourceValue(...) : key ? ... : null`) — регрессии нет, но это
несвязанная правка соседнего кода вне скоупа задачи (правило «скоуп не
расширяется», §2.6). Решение — оставить: правка безвредна и обратный откат
стоит дороже, чем экономит.
2. `src/styles/dialogs.styles.ts` — блок `.colorrow { ... }` переформатирован
с отступа 6 пробелов на 2 без изменения содержимого (чистый шум диффа);
остальной файл сохраняет 6-пробельный отступ. Не влияет на выполнение,
решение — оставить.
3. `demo/smoke_room_temperature_thresholds.mjs` не проверяет отдельно случай
«невалидное значение при скрытом (не-temp) режиме заливки всё равно
блокирует Save» — AC5/AC6 пересекаются здесь только по построению кода
(`tempValid`/`canSaveNew` не зависят от видимости блока), не по
отдельному свидетелю. Логика проста и линейна, дополнительного риска не
вижу — не Medium, просто отмечаю как замеченный, но не обязательный пробел
покрытия.
## AC — таблица «чем доказано / чем краснеет»
| AC | Доказано | Чем краснеет — команда и результат |
|---|---|---|
| AC1 Полное наследование | `test/logic.test.mjs` (`roomTempRangeOf(20,25,null)` и правки space min/max без записи комнаты — читал `_resolvedRoomFills`, наследование идёт через `disp.tempMin/tempMax`, не кэшируется) | покрыто `test/logic.test.mjs`; отдельного мутанта в реестре нет (чистый юнит, читал вручную — резолвер не хранит состояние между вызовами) |
| AC2 Полный override + соседняя комната | `test/logic.test.mjs` + `demo/golden/matrix.mjs` (`room-temperature-dialog-mobile-ru`, partial); соседство проверяется тем, что резолвер вызывается **на комнату**, а не на пространство (цикл `space.rooms.map` в `space-render.ts` и `_resolvedRoomFills`) | `test/room-temperature-renderers.test.mjs` — проверил вручную: откат `space-render.ts` на `disp.tempMin/tempMax` красит тест (см. «ручная мутация» выше) |
| AC3 Частичный override | `test/logic.test.mjs` (own min only / own max only), `test/space-dialog.test.mjs` (`roomTempThresholdDraft('', '19')` → `{min:null,max:19}`) | `--id=room-temperature-min-inherits-wrong-side` → **краснеет** (запускал) |
| AC4 Переставленные границы | `test/logic.test.mjs` (`temp_min:28, temp_max:18` → `{min:18,max:28}`), `test/space-dialog.test.mjs` (`'24,5'`/`'18,5'` с запятой как разделителем — проверил, что `strictNumber` понимает и запятую, и точку) | читал: удаление `Math.min/Math.max` в `roomTempRangeOf` очевидно ломает оба теста выше |
| AC5 Blank/zero/invalid | `test/space-dialog.test.mjs` (`applyRoomTempThresholdDraft` пустое→удаление key, `'0'`→`0`, `'bad'`→`false` без изменения settings) | `--id=room-temperature-blank-becomes-zero` → **краснеет** (запускал) |
| AC6 Скрытые значения не теряются | `demo/smoke_room_temperature_thresholds.mjs` (`hiddenOutsideTemp`, `hiddenDraftRetained`) — зелёный в CI (шард смоков, run 34201299329); писатель (`applyRoomTempThresholdDraft`) вызывается безусловно, не под `if (effectiveFill==='temp')` — проверил чтением в `_saveRoomEdit`/`_roomSettingsFromDialog` | проверено чтением: перенос вызова `applyRoomTempThresholdDraft` под условие `fill==='temp'` регрессировал бы AC6, но живого свидетеля-мутанта на этот шаг нет — единственный пробел эквивалентного веса Low №3 выше |
| AC7 Live preview / parity | `test/room-temperature-renderers.test.mjs`, golden `room-temperature-dialog-desktop-en/mobile-ru` (успешно верифицированы в CI, run 34201299329) | проверил вручную (см. «ручная мутация») — **краснеет** |
| AC8 UI/responsive | golden (обе сцены: desktop-en 900×900, mobile-ru 390×820 dark + partial override), `demo/golden/harness.mjs` бросает при `scrollWidth > clientWidth+1` или неполном наборе полей | golden verify зелёный в CI на этих двух сценах; убрать `flex-wrap: wrap` в `.roomtemprange-fields` сломало бы `narrowHasNoHorizontalScroll` в смоке — не проверял отдельным мутантом, чисто визуальный CSS-инвариант |
| AC9 Backend/compat | `tests_backend/test_validation.py::test_room_temperature_thresholds_are_optional_strict_finite_numbers` — bool/строка/NaN/Infinity отклоняются, `None` принимается, `0`/`24.5` проходят | тест сам по себе явно перебирает контрпримеры (`True`, `"20"`, `nan`, `inf`) внутри одного теста — эквивалент мутации, проверил чтением, что `_finite_number_or_none` действительно вызывает `vol.Invalid` на каждый |
| AC10 Import/export | `tests_backend/test_ha_import_export.py` — full/space round-trip сохраняет `temp_min/max` вместе с `future_room_binding` (недокументированное поле не теряется), plan-only включает оба поля в проекцию и отбрасывает `area`/`future_room` | читал `_ROOM_DISPLAY_FIELDS`: поля добавлены в allowlist рядом с `glow`, а не в HA-identity список — приватность plan-only не ослаблена |
| AC11 Без побочных изменений | `_roomTemp`/`_roomHum` не создают новый climate-pass — сверил, что `roomClimateKey`/`this._climate()` вызываются так же, как до диффа (см. Low №1: рефакторинг чисто косметический) | проверено чтением; `roomTempRangeOf` не читает `hass`/`this._climate()` вообще — чисто математическая функция от трёх аргументов |
| AC12 Локализация/docs | `test/i18n-dead-keys.test.mjs` (`derivedHelpAria.size` 19→20, детерминированно проверяет, что `.help`/`.help.aria` пара существует для всех языков через `_help()`); USER-GUIDE EN/RU и CONFIG-COMPATIBILITY правки читал построчно — соответствуют реальному контракту (наследование, «As the space», единственный потребитель — заливка) | i18n-parity тесты — часть «Фронтенд» job, зелёного на `68462b81` |
## Продуктовое рассуждение (не только AC)
Задача заявлена как закрытие пробела в J5 («Room climate at a glance»,
`docs/SCOPE.md` явно называет «Threshold colouring for room-card metrics» в
списке частично покрытых улучшений). Реализация точно уважает решения
владельца от 07.09: наследование двухуровневое без глобального яруса, нет
ссылок на HA helpers, нет отдельного индикатора «свой диапазон» вне диалога.
Соседние поверхности (room-card, tooltip, источник температуры, HA
service calls) не тронуты — проверил построчно, единственный потребитель
новых полей это `resolveEffectiveRoomFill` через `roomTempRangeOf`/
`roomTempRangeFromDraft`.
«Одно число — один источник» (обязательная проверка): диапазон комфорта
комнаты вычисляется **один раз за кадр** в `_resolvedRoomFills()`
(Full View) и **один раз** в `renderSpaceStatic()` (static card) — оба
вызывают один и тот же чистый резолвер `roomTempRangeOf` (static — напрямую,
Full View — через `roomTempRangeFromDraft`, который добавляет только черновик
активного диалога поверх того же резолвера). Ни диалоговый placeholder, ни
подпись, ни room-card не показывают температурный диапазон отдельно — они
показывают саму температуру, которая этой задачей не переоценивается.
Дублирования числа не вижу.
## Унаследовано / что проверял в этом раунде
Не применимо — это первый заход (r1) код-ревью для #487. Раздел «Унаследовано
из r<N−1>» не требуется (§2.10 относится к повторным раундам). Досрочный
инцидент «Ревью не запускалось» (рассинхрон бандла при ребейзе, 07:41) не
образует раунда: код не читался, вердикта не было, только конфликт
слияния — материал этого r1 это версия ветки уже **после** ребейза.
## Вердикт
Зелёный. AC1–AC12 доказаны — либо автотестом с подтверждённым «умением
падать» (два зарегистрированных мутанта в `scripts/mutation-gate.mjs` плюс
одна ручная мутация для AC7, все три реально прогнаны в этом раунде и
показали красный на дефекте), либо проверкой чтением с явной пометкой.
Находки — три Low, ни одна не требует правки для приёмки (оставлены с
записью выше). Дешёвые гейты подтверждены зелёным Validate на точном SHA
`68462b81` и его функционально идентичном родителе `89deee0f` (полный
тяжёлый набор, включая golden и все три шарда смоков). Golden-провенанс
проверен отдельно из-за красного/отменённого `Baseline-Reviewed` прогона —
семантика подтверждена (новые сцены закономерно красят verify до принятия),
подмены эталонов не найдено.
---
<!-- material-anchors: сгенерировано конвейером (#414) -->
## Материал раунда
- Ветка: `issue/487-room-temperature-thresholds`, коммит `68462b81e08b` — ребейз его осиротит, и это нормально: ниже якоря, которые ребейз не меняет.
- Дерево материала: `5e50ffb3debd91974b494e4ba49d073a8402e583`
```
git log --all --format='%H %T' | grep 5e50ffb3debd
```
- ТЗ `docs/specs/487-room-temperature-thresholds.md`, блоб `d1d33e782d3edd6fd7e393c6e372a7d76439d05e`
```
git log --all --find-object=d1d33e782d3edd6fd7e393c6e372a7d76439d05e -- docs/specs/487-room-temperature-thresholds.md
```