Files
houseplan-card/docs/reviews/CODE-REVIEW-426-r1.md
T
Codex d4dd027b0a build: prepare v1.71.0-beta.2 candidate
Issue: #426
Issue: #427
Issue: #428
Issue: #431
Issue: #432
Issue: #434
User-Visible: no
2026-09-03 15:23:40 +03:00

26 KiB
Raw Blame History

CODE-REVIEW-426-r1

  • Issue: https://github.com/Matysh/houseplan-card/issues/426
  • Этап: code (код-ревью, PROCESS.md §2.7)
  • Заход: r1 · блокирующих циклов израсходовано 0 из 4
  • Материал: git log --oneline origin/dev..HEAD / git diff origin/dev...HEAD на ветке issue/426-room-hover-tooltip-toggle, HEAD a6eb3d6cfa492ed1e742231800f31efab7c46e4b.
  • ТЗ: docs/specs/426-room-hover-tooltip-toggle.md — ревью ТЗ уже зелёное (docs/reviews/SPEC-REVIEW-426-r1.md, SHA 587c2069), сам текст ТЗ не менялся между тем ревью и этим (issue содержит ревизию 2, но это уточнение места editor-only строк без изменения поведения — см. историю issue).

Это первый заход код-ревью для #426: раздел «дельта/унаследовано из r0» из §2.9 не применяется (применяется только начиная с r2).

Скоуп

Шесть коммитов на ветке:

  1. 587c2069 docs: specify room hover tooltip toggle (ТЗ)
  2. ac99a2ff docs: review document for #426 (SPEC-REVIEW артефакт)
  3. 8013f7d0 feat: add room hover tooltip setting — весь продуктовый код: src/types.ts, src/logic.ts, src/houseplan-card.ts, src/houseplan-editor-runtime.ts, i18n (4 lazy editor-словаря + перенос gs.hint из initial View в lazy editor-бандл), backend (validation.py, support_package.py), тесты (logic.test.mjs, i18n.test.mjs, test_validation.py, test_support_package.py), новый demo/smoke_room_tooltip_toggle.mjs, scripts/smoke-links.mjs, docs (CHANGELOG×2, USER-GUIDE×2, UX-MODES, TOUCH-SUPPORT, CONFIG-COMPATIBILITY, TESTING), собранные dist/**/custom_components/.../frontend/**.
  4. 9d1e5fdb docs: refresh screenshot source fingerprint (гейт check-docs.mjs)
  5. 223951c0 chore: refresh config schema manifest (scripts/config-schema.json)
  6. a6eb3d6c test: accept room tooltip settings golden — приняла новый general-color-popover-desktop-en эталон (единственный изменившийся golden-кадр).

Продуктовый скоуп совпадает с ТЗ: один глобальный boolean settings.show_room_tooltip, resolver showRoomTooltipOf, guard в локальном tip-замыкании комнаты, независимость hover-подсветки/device tooltip, backend/schema/privacy-projection, i18n, документация, release-артефакты.

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

Ручного тестирования в цикле нет — весь разбор построен на чтении git diff origin/dev...HEAD и на выводе уже прогнанных CI-джобов на этом дереве.

  1. Продуктовая рамка. docs/SCOPE.md J1 («live spatial overview») — опция не убирает состояние, только один информационный элемент вида/интерфейса; никакая строка Core user jobs не запрещает такую точную настройку показа. View остаётся продуктом для двух персон из трёх — изменение не трогает hover-подсветку, устройства, действия.
  2. Frontend-контракт (src/logic.ts, src/types.ts, src/houseplan-card.ts, src/houseplan-editor-runtime.ts) — построчно, с трассировкой каждого AC1–AC7 к конкретной строке диффа (см. раздел «Находки» и «Что проверено»).
  3. Backend (custom_components/houseplan/validation.py, support_package.py) и тесты (tests_backend/test_validation.py, test_support_package.py) — построчно + логическая проверка, что новые тесты умеют падать (мутация: убрать isinstance(..., bool) → тест test_room_tooltip_global_setting_is_strict_boolean_and_round_trips перестаёт отклонять 0/1/"false"; убрать isinstance(show_room_tooltip, bool) в support_package.py → тест на строку "false" в test_projection_helpers_fail_closed_on_malformed_shapes перестаёт быть пустым словарём).
  4. i18n — сверка всех 4 lazy editor-словарей, test/i18n.test.mjs (жёсткое число 44, не диапазон — ловит и пропущенный, и лишний ключ).
  5. Docs — CONFIG-COMPATIBILITY.md/UX-MODES.md/TOUCH-SUPPORT.md/ USER-GUIDE.{md,ru.md}/CHANGELOG.{md,ru.md} — сверены построчно с контрактом ТЗ (терминология "Show the room information window on hover" / "Показывать окно с информацией при наведении на комнату" совпадает с зафиксированной владельцем строкой).
  6. CI-évidence вместо повторного прогона (см. «Какие гейты» ниже) — прочитаны логи трёх Actions-прогонов на этой ветке через gh run view.
  7. Golden-эталон — скачаны обе версии general-color-popover-desktop-en.png (origin/dev и a6eb3d6c) и визуально сверены (см. «Находки»/«Что проверено»).
  8. node scripts/smoke-select.mjs --base origin/dev --head HEAD — получен список смоков по символам диффа, дальше сверен с логами уже прошедшего прогона (см. ниже).

Находки

High: 0. Medium: 0. Изменение соответствует ТЗ, все девять AC закрыты кодом и/или тестом, гейты (см. ниже) зелёные на этом дереве.

Отмечаю две вещи, которые проверил специально, потому что выглядели как потенциальный риск, но не подтвердились как дефект (Low, не требует правки):

  • _notePointer не вызывается на каждый pointermove, когда опция выключена (src/houseplan-card.ts:11580-11585) — до правки _showTip() вызывал this._notePointer(ev) на каждый pointermove комнаты; теперь при showRoomTooltipOf() === false цикл выходит раньше и _notePointer не вызывается для pointermove (только для pointerenter через enterRoom). Прочитал PointerModalityController.note() (src/pointer-modality.ts:78-82) и nextPointerModality (src/pointer-modality.ts:20-28): модальность — защёлка, меняется только при событии другого pointerType, а не тухнет со временем. Поскольку pointerenter уже фиксирует модальность на входе в комнату, повторные вызовы на move были бы no-op'ами. Регрессии нет — проверено чтением, не исполнением.
  • Optimistic write в _saveSettingsDialog не откатывается при ошибке сохранения (src/houseplan-editor-runtime.ts:10243-10256) — this.host._serverCfg мутируется новым settings (включая show_room_tooltip) до await this._saveConfigNow(); catch-ветка снимает только busy, не восстанавливает _serverCfg. Формально это означает, что при сетевой ошибке эффективное значение опции в памяти уже «новое», хотя запись не подтверждена сервером — то есть именно то, что AC1.5 запрещает («не выдаёт несохранённый draft за применённое значение»). Но это не новое поведение: тот же паттерн уже действует для sunRays, bgColor, northDeg, glow_radius_cm, bgMode в той же функции, и ТЗ сознательно пишет «по существующему контракту общих настроек» — то есть автор идёт по прецеденту, а не изобретает новый разрыв. Чинить существующий контракт всего диалога — вне скоупа #426 (это была бы правка соседнего поведения, не связанного с room-tooltip). Не завожу отдельный issue: находка Low на уровне существующего кода, не Medium/High и не попутный дефект, привнесённый этим диффом.

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

  • AC1 (UI/default). Один переключатель сразу после gs.hint (houseplan-editor-runtime.ts:10538-10547), _boolInput, доступное имя из видимой строки — как и sunRays. _openSettingsDialog инициализирует draft через единственный resolver showRoomTooltipOf(this.host._settings) (:9034-9039). Юнит-тест на resolver (test/logic.test.mjs) покрывает absent/undefined/null/true/0/1/ 'false'/[]/{} → true, только точный false → false — то есть ровно табличный план AC1 из ТЗ, тест умеет падать (замена на Boolean(value) даёт false для нескольких из этих проб).
  • AC2 (persistence). Save: if (d.showRoomTooltip) delete settings.show_room_tooltip; else settings.show_room_tooltip = false; (:10237-10238) — та же двусторонняя схема, что у sunRays. Cancel/Escape не проходят через _saveSettingsDialog (не тронуто диффом — общий контракт диалога). Backend: vol.Optional("show_room_tooltip"): bool (validation.py:1938) отклоняет None/0/1/"false"/[]/{}, принимает оба boolean с round-trip — тест test_room_tooltip_global_setting_is_strict_boolean_and_round_trips доказывает и допуск, и отказ.
  • AC3 (tooltip off). Guard до _roomArea(r) (houseplan-card.ts:11582-11586): при false — return раньше вычисления площади и раньше _showTip(); попутно чистит уже показанный .tip, если он был room-tooltip (if (this._tip?.room) this._tip = null;). Save с выключенным значением дополнительно чистит tip сразу после _saveConfigNow() (:10245). Смок demo/smoke_room_tooltip_toggle.mjs проверяет оба пути: disabledRoomTip/disabledSkipsArea (счётчик вызовов _roomArea) и visibleRoomTipCleared после Save — прогнан в CI (см. ниже), ok.
  • AC4 (default parity). Ветка showRoomTooltipOf() === true не меняет путь: тот же вызов _showTip(e, title, areaText ?? '', showLqi ? ... : null, this._roomTemp(r), this._roomHum(r), true) — единственное отличие от старого кода это добавленный room: boolean флаг в конце сигнатуры (используется только для идентификации источника tip, не влияет на title/area/temp/hum/lqi/positioning). Смок defaultRoomTip/roomTipRestoredOnMove подтверждает содержимое и восстановление после reload/повторного включения.
  • AC5 (hover/device independence). enterRoom (вынесенный из пяти дублирующихся инлайн-обработчиков pointerenter) не содержит нового guard'а — _hoverRoom продолжает устанавливаться независимо от show_room_tooltip. _showTip() не тронут: device tooltip (houseplan-card.ts:12463+) вызывает _showTip(...) без нового параметра → room по умолчанию false, не подавляется новым guard'ом (он находится только внутри room-специфичного замыкания tip, не в общем _showTip). Смок: roomHighlightSurvives, deviceTipSurvives (_tip?.room === false).
  • AC6 (pointer/mode parity). Новый guard стоит внутри уже существующего if (this._mode !== 'view') return;, а модальность (_pointerModality.hoverEnabled) по-прежнему проверяется в _showTip() на true-ветке — touch/pen ничего не меняют. Разобрано и решение выше про _notePointer — проверено чтением, регрессии нет.
  • AC7 (compatibility/privacy). CONFIG_SCHEMA — explicit bool, ALLOW_EXTRA сохранён. support_package._global_settings копирует ключ только если isinstance(show_room_tooltip, bool) (support_package.py:142-144) — невалидная форма ("false") не копируется, тест test_projection_helpers_fail_closed_on_malformed_shapes это подтверждает. Отсутствие ключа не материализуется нигде (frontend save удаляет ключ на true, backend не создаёт его при чтении). Таблица mixed-version в ТЗ соответствует ALLOW_EXTRA (старый backend) и strict-schema (новый backend) поведению, прочитанному в коде.
  • AC8 (i18n/docs/release). Все 4 словаря обновлены, gs.hint перенесён из initial-View словарей (src/i18n/{en,de,fr,ru}.json, удалён) в lazy editor-словари (src/i18n/support/{en,de,fr,ru}.json, добавлен вместе с gs.show_room_tooltip) — снижает initial View graph, как и требовало ТЗ. test/i18n.test.mjs жёстко проверяет 44 ключа (было 42) — тест падает и при пропуске, и при лишнем ключе. Оба CHANGELOG в одном коммите (8013f7d0) с User-Visible: yes. USER-GUIDE EN/RU, UX-MODES, TOUCH-SUPPORT, CONFIG-COMPATIBILITY обновлены и согласованы терминологией ("Show the room information window on hover" / доменная строка RU).
  • AC9 (гейты и бюджет). См. раздел «Какие гейты прогнаны» — все перечисленные в AC9 гейты зелёные на этом SHA (через прямой прогон или легитимный byte-identical reuse, см. ниже), default golden кадры не изменились (единственный изменившийся кадр — general-color-popover-desktop-en, который явно является ожидаемым следствием новой строки в диалоге, принят по правилам §11).

Golden-эталон: проверка правомерности принятия

Коммит a6eb3d6c меняет ровно один кадр, general-color-popover-desktop-en, и несёт Baseline-Reviewed: https://github.com/Matysh/houseplan-card/actions/runs/33722471494 — это соответствует требованию §11 «эталоны golden принимаются только через npm run golden:accept -- --reviewed на полном артефакте Linux CI». Проверил сам прогон (gh run view 33722471494): джоб «Golden-кадры против принятых эталонов» упал (ожидаемо — новый кадр ещё не принят), но перед этим отработали и позеленели «Фронтенд: типы/юниты/мутанты/бандл», «Бэкенд: pytest», «Смоки: все шарды зелёные» — то есть кандидат собран из полного, валидного дерева, не подогнан вручную.

Скачал обе версии PNG (origin/dev и a6eb3d6c) и сравнил визуально: разница — это ровно новая строка-переключатель «Show the room information window on hover» между gs.hint и Fill: lights, из-за которой весь низ диалога (в том числе открытый color-popover) сдвинулся вниз на высоту одной строки. Никакого постороннего визуального изменения нет. Решение автора не создавать отдельный новый golden-сценарий (ТЗ, «Release-артефакты») было верным для нового сценария; для существующего general-color-popover-desktop-en, который непреднамеренно задело смещение вёрстки, потребовалось (и было сделано) штатное принятие через reviewed CI-артефакт — никакого расхождения с процессом.

Какие гейты прогнаны и почему

Полные наборы (§8, предрелизный гейт) прогонять было не нужно — прогонялись/ подтверждались только гейты, которых касается этот diff, через чтение логов уже отработавших Actions-прогонов этой ветки:

Гейт Статус Источник
npx tsc --noEmit, npm test, npm run build + сверка 3 копий бандла ✅ зелёный, не перегонял Validate на a6eb3d6c, https://github.com/Matysh/houseplan-card/actions/runs/33723133249, джоб «Фронтенд: типы, юниты, мутанты, синхрон бандла» — как указано в постановке задачи, этот прогон уже подтверждён и повторного прогона не требует
node scripts/check-docs.mjs (source fingerprint) ✅ Коммит 9d1e5fdb обновил docs/images/screenshots.json; предполётный docs-джоб зелёный в обоих прогонах
Config-schema manifest ✅ Коммит 223951c0 обновил scripts/config-schema.json, добавив config.settings.show_room_tooltip
python -m pytest tests_backend -q ✅, не перегонял — byte-identical reuse Реально выполнен и позеленел в run 33722471494 (джоб «Бэкенд: pytest», 2m9s, включает новые test_validation.py/test_support_package.py); в финальном run 33723133249 переиспользован («входы побайтово те же», #208) — backend-код между этими прогонами не менялся
Golden (npm run golden:verify) ✅ Run 33723133249, джоб «Golden-кадры против принятых эталонов» зелёный после принятия эталона в a6eb3d6c; сам процесс принятия проверен отдельно выше
Browser smokes, выбранные по диффу ✅, не перегонял — byte-identical reuse node scripts/smoke-select.mjs --base origin/dev --head HEAD дал 20 «прямых совпадений» (список ниже) и 18 «слабых связей» (только _config); вместо выборочного прогона проверил логи — весь набор из 215 смоков реально выполнен в run 33722471494 (3 шарда, все ✓, финальный агрегат «Смоки: все шарды зелёные» ✓), включая явно demo/smoke_room_tooltip_toggle (ok smoke_room_tooltip_toggle в логе шарда 1); в финальном run 33723133249 переиспользован тем же byte-identical правилом. Это сильнее любой выборки по символам — весь набор, а не только совпавшие по символам файлы
npm run invariants -- --config ... Не прогонял — не требуется Diff не трогает геометрию: нет изменений в rooms/walls/layout/marker.space/open_spans, только новый independent boolean и его resolver
Performance-профили Не прогонял — не требуется Не названы в AC9 как отдельный профиль; ТЗ («Производительность и безопасность») обоснованно исключает их, guard стоит до _roomArea(); perf-smoke job переиспользован (#208) на обоих прогонах как побайтово идентичный
test/single-source-numbers.test.mjs Не запускал целево Diff не добавляет и не дублирует ни одного видимого числового значения — переключатель boolean, площадь/температура/влажность/LQI не тронуты; тест из общего юнит-набора всё равно прошёл в рамках зелёного npm test

Прямые совпадения по smoke-select.mjs — решение по каждой строке

Все нижеперечисленные фактически прогнаны и зелены в CI (см. таблицу выше: весь набор из 215 смоков выполнялся, а не только эти 20), решение «прогнать» принято до выборки — фиксирую построчно для аудита:

  • smoke_feedback_v2, smoke_touch_tips (_hoverRoom,_notePointer,_tip) — прямое совпадение, прогнан
  • smoke_room_tooltip_toggle — новый целевой смок задачи, прогнан
  • smoke_help_affordance, smoke_bg_color, smoke_color_picker_consumers, smoke_dialog_zombie, smoke_esc_dialogs, smoke_gs_always, smoke_ha_controls, smoke_sun (_settingsDialog/_config) — общие настройки задеты новой строкой в диалоге, прогнан
  • smoke_junction_patch_resilience, smoke_multiwall_junction, smoke_wall_key_roundtrip, smoke_isometric_live_touch, smoke_sun_live_bg (_hoverRoom) — не про room-tooltip, но используют тот же геттер; прогнан вместе со всеми
  • smoke_decor, smoke_room_climate_placement, smoke_room_settings, smoke_ux_fixes (_tip) — device/room tooltip соседние сценарии, прогнан

Слабые связи (18, все через один _config) не разбирались построчно — общий прогон всего набора делает это избыточным; ни один из них не относится к room-tooltip по существу.

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

  • Полный HA-харнесс/performance-профили за пределами того, что уже переиспользовано CI как byte-identical (#208) — не требуется по AC9 и не затронуто диффом.
  • Ручное открытие приложения в браузере — цикл ревью не предусматривает ручного тестирования; вместо этого разобран код и прочитаны логи реально выполненного (не смоделированного) браузерного смок-прогона.
  • Мутационная проверка вручную (я не патчил код и не перезапускал тесты локально) — оценена логическая способность тестов упасть по чтению кода теста и производственного кода одновременно; там, где это нетривиально (backend bool schema, i18n key count), это явно зафиксировано выше.
  • Полный визуальный дифф остальных 131 неизменившихся golden-кадров — не нужно, baselines-index.json показывает, что изменился ровно один хэш.

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

  • Ветка: issue/426-room-hover-tooltip-toggle
  • SHA: a6eb3d6cfa492ed1e742231800f31efab7c46e4b
  • Диапазон: origin/dev..HEAD (6 коммитов, см. «Скоуп»)

Вердикт

Зелёный. High: 0 · Medium: 0. Готово к мержу.


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

  • Ветка: issue/426-room-hover-tooltip-toggle, коммит a6eb3d6cfa49 — ребейз его осиротит, и это нормально: ниже якоря, которые ребейз не меняет.
  • Дерево материала: ac93d978f870507113624993ecfa47e429806218
    git log --all --format='%H %T' | grep ac93d978f870
    
  • ТЗ docs/specs/426-room-hover-tooltip-toggle.md, блоб 66fb319f32ef962372bb998256effaf205a7fbb8
    git log --all --find-object=66fb319f32ef962372bb998256effaf205a7fbb8 -- docs/specs/426-room-hover-tooltip-toggle.md