Files
houseplan-card/docs/reviews/CODE-REVIEW-487-r1.md
T
claude[bot] 3a702b3124
Проверка (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
docs: review document for #487
Issue: #487
User-Visible: no
2026-09-08 08:11:43 +00:00

23 KiB
Raw Blame History

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 до принятия), подмены эталонов не найдено.


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

  • Ветка: 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