Issue: #426 Issue: #427 Issue: #428 Issue: #431 Issue: #432 Issue: #434 User-Visible: no
26 KiB
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, HEADa6eb3d6cfa492ed1e742231800f31efab7c46e4b. - ТЗ:
docs/specs/426-room-hover-tooltip-toggle.md— ревью ТЗ уже зелёное (docs/reviews/SPEC-REVIEW-426-r1.md, SHA587c2069), сам текст ТЗ не менялся между тем ревью и этим (issue содержит ревизию 2, но это уточнение места editor-only строк без изменения поведения — см. историю issue).
Это первый заход код-ревью для #426: раздел «дельта/унаследовано из r0» из §2.9 не применяется (применяется только начиная с r2).
Скоуп
Шесть коммитов на ветке:
587c2069docs: specify room hover tooltip toggle (ТЗ)ac99a2ffdocs: review document for #426 (SPEC-REVIEW артефакт)8013f7d0feat: 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/**.9d1e5fdbdocs: refresh screenshot source fingerprint (гейтcheck-docs.mjs)223951c0chore: refresh config schema manifest (scripts/config-schema.json)a6eb3d6ctest: 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-джобов на этом
дереве.
- Продуктовая рамка.
docs/SCOPE.mdJ1 («live spatial overview») — опция не убирает состояние, только один информационный элемент вида/интерфейса; никакая строка Core user jobs не запрещает такую точную настройку показа. View остаётся продуктом для двух персон из трёх — изменение не трогает hover-подсветку, устройства, действия. - Frontend-контракт (
src/logic.ts,src/types.ts,src/houseplan-card.ts,src/houseplan-editor-runtime.ts) — построчно, с трассировкой каждого AC1–AC7 к конкретной строке диффа (см. раздел «Находки» и «Что проверено»). - 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перестаёт быть пустым словарём). - i18n — сверка всех 4 lazy editor-словарей,
test/i18n.test.mjs(жёсткое число44, не диапазон — ловит и пропущенный, и лишний ключ). - 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" / "Показывать окно с информацией при наведении на комнату" совпадает с зафиксированной владельцем строкой). - CI-évidence вместо повторного прогона (см. «Какие гейты» ниже) —
прочитаны логи трёх Actions-прогонов на этой ветке через
gh run view. - Golden-эталон — скачаны обе версии
general-color-popover-desktop-en.png(origin/devиa6eb3d6c) и визуально сверены (см. «Находки»/«Что проверено»). 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 через единственный resolvershowRoomTooltipOf(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— explicitbool,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— ребейз его осиротит, и это нормально: ниже якоря, которые ребейз не меняет. - Дерево материала:
ac93d978f870507113624993ecfa47e429806218git log --all --format='%H %T' | grep ac93d978f870 - ТЗ
docs/specs/426-room-hover-tooltip-toggle.md, блоб66fb319f32ef962372bb998256effaf205a7fbb8git log --all --find-object=66fb319f32ef962372bb998256effaf205a7fbb8 -- docs/specs/426-room-hover-tooltip-toggle.md