Issue: #487 User-Visible: no
23 KiB
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, backendvalidation.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 (не блокирует, зафиксировано без правки):
src/houseplan-card.ts, метод_roomTemp/_roomHum(около строки 12559) — попутный рефакторинг, не требуемый задачей: две задокументированные функции с раздельными JSDoc-комментариями схлопнуты в однострочный тернарник без комментариев. Поведение идентично (сравнил построчно: былоif (src) return sourceValue(...); ...; return key ? ... : null;, сталоsrc ? sourceValue(...) : key ? ... : null) — регрессии нет, но это несвязанная правка соседнего кода вне скоупа задачи (правило «скоуп не расширяется», §2.6). Решение — оставить: правка безвредна и обратный откат стоит дороже, чем экономит.src/styles/dialogs.styles.ts— блок.colorrow { ... }переформатирован с отступа 6 пробелов на 2 без изменения содержимого (чистый шум диффа); остальной файл сохраняет 6-пробельный отступ. Не влияет на выполнение, решение — оставить.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 до принятия),
подмены эталонов не найдено.
Материал раунда
- Ветка:
issue/487-room-temperature-thresholds, коммит68462b81e08b— ребейз его осиротит, и это нормально: ниже якоря, которые ребейз не меняет. - Дерево материала:
5e50ffb3debd91974b494e4ba49d073a8402e583git log --all --format='%H %T' | grep 5e50ffb3debd - ТЗ
docs/specs/487-room-temperature-thresholds.md, блобd1d33e782d3edd6fd7e393c6e372a7d76439d05egit log --all --find-object=d1d33e782d3edd6fd7e393c6e372a7d76439d05e -- docs/specs/487-room-temperature-thresholds.md