Files
houseplan-card/docs/reviews/CODE-REVIEW-358-r1.md
T
2026-08-28 18:59:48 +00:00

18 KiB
Raw Blame History

CODE-REVIEW-358-r1

Issue: #358 · этап: code · заход r1 · блокирующих циклов израсходовано 0/2 SHA ревью: bb3c1fd1 (HEAD ветки issue/358-cold-view-vacuum, детач от origin/dev)

Скоуп

Систематический аудит после #357: единственный оставшийся достижимый из холодного View жёсткий стаб _vacMapId — вызывается внутри willUpdate (_captureRenderDeviceSnapshot) для любого пылесоса с телеметрией. На холодной вкладке бросает Houseplan editor runtime is not loaded, исключение внутри willUpdate убивает весь цикл обновления Lit — карточка замирает целиком, не только клик, как в #357.

Задача — light track (small), ТЗ в теле issue, ревью по PROCESS.md §2.7. Диф: src/houseplan-card.ts, src/houseplan-editor-runtime.ts, новый смок demo/smoke_cold_view_vacuum.mjs, мутант в scripts/mutation-gate.mjs, запись в scripts/smoke-links.mjs, оба changelog, синхронизация трёх копий бандла и docs/images/screenshots.json (фингерпринт документации).

Соответствие job из docs/SCOPE.md: чинит регресс в основном View-пути (J1/J2 — «живая пространственная карта» не должна замирать), правка не расширяет скоуп.

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

Материал — git log --oneline origin/dev..HEAD (один коммит) и git diff origin/dev...HEAD. Ручного тестирования не было, весь разбор — чтением диффа плюс браузерные смоки локально.

Дешёвые гейты (нет зелёного Validate на этом SHA — прогнал сам):

Гейт Результат
npx tsc --noEmit чисто, без вывода
npm test tests 1499 / pass 1498 / fail 0 / skipped 1 — совпадает с числом, заявленным автором
npm run build успешно, dist собран
npm run bundle:sync пересобрал; git status --short пуст — три копии бандла (dist, custom_components/.../frontend, demo/srv/assets) идентичны коммиту
node scripts/check-docs.mjs Documentation checks passed (7 files, 10 external links) — diff трогает src/**, фингерпринт скриншотов в коммите уже обновлён и совпадает

Инварианты модели (npm run invariants) — не прогонял: диф не трогает геометрию, layout, marker.space, рёбра/толщину стен, open_spans. Не применимо к этой задаче.

Браузерные смоки — выбор через node scripts/smoke-select.mjs --base <origin/dev SHA> --head HEAD (изменено 2 файла src/**, 11 символов проекта на изменённых строках, 16 прямых совпадений):

demo/smoke_cold_view_vacuum.mjs   ← _editorRuntime, _vacMapId, willUpdate
demo/smoke_binding_picker.mjs     ← _planHass
demo/smoke_cold_view_toggle.mjs   ← _editorRuntime
demo/smoke_controls.mjs           ← DevItem
demo/smoke_decor_text.mjs         ← _decorShapeDbl
demo/smoke_decor.mjs              ← _decorShapeDown
demo/smoke_drag_bounds.mjs        ← _decorShapeDown
demo/smoke_furniture.mjs          ← _decorShapeDbl
demo/smoke_grid_scale_invariance.mjs ← _editorRuntime
demo/smoke_ha_controls.mjs        ← _editorRuntime
demo/smoke_lazy_editor_chunk.mjs  ← _editorRuntime
demo/smoke_live_text.mjs          ← _decorShapeDbl
demo/smoke_partition_openings.mjs ← _editorRuntime
demo/smoke_static_icon.mjs        ← _planHass
demo/smoke_vacuum_firstuse.mjs    ← _vacCalConfirm
demo/smoke_ws_resilience.mjs      ← willUpdate

Решение по каждой строке:

  • Прогнал (прямое совпадение на реально изменённом символе, либо AC-обязательный):
    • smoke_cold_view_vacuum.mjs — новый, доказывает AC1/AC3 — OK
    • smoke_vacuum.mjs — назван AC2 в issue напрямую (нет в выдаче smoke-select, т.к. не ссылается на изменённые символы текстом, но AC требует его явно) — OK
    • smoke_vacuum_firstuse.mjs — назван AC2, плюс прямое совпадение на _vacCalConfirm (гард К2) — OK
    • smoke_decor.mjs — прямое совпадение на _decorShapeDown, метод реально изменён (К2-гард) — OK
    • smoke_drag_bounds.mjs — то же самое совпадение на _decorShapeDown — OK
    • smoke_cold_view_toggle.mjs — тот же класс регрессии (#357), родственный контракт «холодная вкладка + _editorRuntime» — OK
    • smoke_lazy_editor_chunk.mjs — прямой контракт ленивой загрузки редакторского чанка, который меняющийся гард _vacCalConfirm мог задеть — OK
  • Не прогонял (связь через широко используемое имя, символ сам не менялся):
    • smoke_binding_picker.mjs, smoke_static_icon.mjs — совпадение на _planHass, который в диффе фигурирует только как непереименованный параметр по умолчанию, не как изменённая логика.
    • smoke_controls.mjs — совпадение на типе DevItem, использованном в сигнатуре без изменения самого типа.
    • smoke_decor_text.mjs, smoke_furniture.mjs, smoke_live_text.mjs — совпадение на _decorShapeDbl, парном методе, который сам не тронут (гард получил только _decorShapeDown).
    • smoke_grid_scale_invariance.mjs, smoke_ha_controls.mjs, smoke_partition_openings.mjs, smoke_ws_resilience.mjs — совпадение на широких именах (_editorRuntime, willUpdate), не специфичных для вносимого изменения; уже покрыты по духу выбранными cold-view смоками.

Результаты (все зелёные, без правки ассертов):

smoke_cold_view_vacuum   → OK (все 10 подпроверок true)
smoke_vacuum             → OK
smoke_vacuum_firstuse    → OK (13 подпроверок true)
smoke_decor              → OK (13 подпроверок true)
smoke_drag_bounds        → OK (12 подпроверок true)
smoke_cold_view_toggle   → OK (12 подпроверок true)
smoke_lazy_editor_chunk  → OK (12 подпроверок true)

AC3 (мутант «вернуть делегацию _vacMapId в runtime»): скрипт scripts/mutation-gate.mjs в этом окружении не принимает выборочный --id и вместо точечного прогона выполняет полный «чистый прогон» всех гардов подряд; он остановился на несвязанном с задачей FAIL … красный без мутанта: node scripts/backend-test-guard.mjs … tests_backend/test_frontend_assets.py — причина No module named pytest (в этом контейнере не установлен Python-бэкенд-харнесс, см. AGENTS.md «Backend» — pytest без Home Assistant не то же самое, что полный харнесс, а тут его вообще нет). Это ограничение окружения ревьюера, а не дефект диффа: диф не трогает custom_components/**/*.py вообще.

Проверил мутант вручную вместо гейта: применил патч мутанта (cold-view-vacuum-mapid-delegated) к рабочей копии src/houseplan-card.ts руками (тот же find/replace, что в scripts/mutation-gate.mjs), пересобрал (npm run build && node scripts/bundle-sync.mjs) и прогнал node demo/smoke_cold_view_vacuum.mjs:

page.evaluate: Error: Houseplan editor runtime is not loaded
    at Dm._editorRuntimeOrThrow (...)
    at Dm._vacMapId (...)
    at Dm._captureRenderDeviceSnapshot (...)
    at Dm.willUpdate (...)

Смок падает некрасиво (необработанное исключение вместо контролируемого FAIL) — это ожидаемо, ровно как описывает issue: willUpdate рушится целиком. Мутант убит. После проверки восстановил файл из бэкапа, пересобрал и снова прогнал смок на чистом коде — зелёный, git status --short пуст.

python -m pytest tests_backend -q — не прогонял: диф не трогает custom_components/houseplan/**/*.py; изменённые файлы под custom_components/houseplan/frontend/** — класс D (генерируемое), получены исключительно из npm run bundle:sync, вручную не редактировались (проверено побайтовой идентичностью после локального пересобранного прогона).

npm run golden:verify — не прогонял: диф не меняет геометрию, стили и слои рендера; единственное визуальное изменение — фингерпринт скриншотов документации (регенерация хэша из-за правки src/**, покрыта check-docs.mjs), а не новый визуальный контент.

Performance-профили — не названы в AC, диф не трогает производительность-чувствительные пути (только выбор map-id и два гарда).

Находки

Не найдено. Правка — механический перенос владения (карта вместо делегирования в редакторский runtime) по уже принятому образцу #357, плюс два точечных гарда по образцу уже принятого _decorShapeDbl и остальных редакторских диалогов. Инвариант HP-1541-01 (selected_map: 0 как валидный id, nullish-, а не truthy-проверка) перенесён дословно вместе с комментарием — сверено построчно, диф не меняет ни один символ внутри тела метода при переносе.

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

  • К1 (перенос _vacMapId): _vacMapId теперь публичный метод HouseplanCard, тело идентично прежней реализации в houseplan-editor-runtime.ts (только откуда чтение this → this, без изменения логики). HouseplanEditorHostPort пополнен записью _vacMapId: (d, tele, planHass?) => string — типы совпадают с сигнатурой карты. Runtime-версия теперь однострочно делегирует в this.host._vacMapId(...) — тот же приём, что _toggleIntent в #357.
  • К2 (гарды):
    • _decorShapeDown получил if (!this._editorRuntime) return; — точно такой же гард, как у _decorShapeDbl (#337); проверено смоками smoke_decor.mjs/smoke_drag_bounds.mjs (не бьёт существующее поведение) и новым холодным смоком (тихий no-op на pointerdown).
    • Рендер _vacCalConfirm обёрнут this._editorRuntime ? — выровнено с соседними редакторскими диалогами (_deviceInbox, _markerDialog, _rulesDialog, _settingsDialog в том же шаблоне).
  • К3 (холодный смок): demo/smoke_cold_view_vacuum.mjs — инжектирует пылесос с реальной телеметрией (vacuum_position, selected_map: 0), проверяет: карточка рендерится на холодной вкладке; три последовательных кадра телеметрии подтверждают, что willUpdate не просто пережил первый рендер, а продолжает работать (secondFrameCommitted, thirdFrameCommitted сверяют, что снэпшот реально обновился, а не просто существует); mapIdResolved проверяет HP-1541-01 (selected_map: 0 → '0'); noEditorRuntimeRequest — редакторский чанк не запрошен; noPageErrors — исключений нет; декор-гард (К2) проверен pointerdown по реальному DOM-узлу .dshape.
  • AC2: smoke_vacuum.mjs, smoke_vacuum_firstuse.mjs зелёные без правки ассертов (прогнал сам, см. таблицу выше) — регресс в тёплом пути не внесён.
  • Трейлеры: Issue: #358, User-Visible: yes на коммите; оба changelog (docs/CHANGELOG.md, docs/CHANGELOG.ru.md) правлены в том же коммите, формулировка на пользовательском языке («карточка замирает» / «freezes»), без утечки внутренних имён методов.
  • Трассируемость аудита: scripts/smoke-links.mjs пополнен записью, привязывающей vacMapIdWithFallback/vacMapIdFromAttrs/readVacTelemetry к новому смоку — будущий diff по этим символам сам найдёт нужный смок.
  • Мутационный гейт: новый мутант cold-view-vacuum-mapid-delegated добавлен, guard указывает на правильный смок; убит вручную (см. выше).
  • Одно число — один источник: диф не добавляет и не меняет пользовательски видимую величину, отображаемую параллельно в двух местах (map id — внутреннее значение для сопоставления координат телеметрии, не рендерится пользователю как число/подпись). Проверка test/single-source-numbers.test.mjs — часть npm test, прошла в общем прогоне (1498/1498).
  • Границы аудита (раздел issue «Результат аудита в целом»): утверждение «остальные жёсткие заглушки недостижимы из холодного View» не перепроверял по всем 305 узлам графа — это отдельное объёмное статическое исследование вне этого диффа; для целей этого code review важно только то, что сам диф закрывает названные в AC1–AC4 дыры, что подтверждено смоками и мутантом.

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

  • Полный набор demo/smoke_*.mjs (200 файлов) — не запускал; выбор ограничен smoke-select.mjs + AC-именованными смоками (см. таблицу). Полный прогон — обязанность пред-релизного гейта (PROCESS.md §8), не этого ревью.
  • npm run invariants — диф не трогает геометрию/толщину стен/layout.
  • python -m pytest tests_backend -q — диф не трогает Python-код бэкенда; окружение ревьюера к тому же не имеет pytest вовсе.
  • npm run golden:verify — диф не меняет визуальный рендер; единственное затронутое изображение (01-view-desktop.png) — побочный продукт пересборки бандла (иная хэш-сумма бандла → новый скриншот с тем же содержимым), покрыт check-docs.mjs, не самостоятельный визуальный гейт.
  • Performance-профили — не названы в AC, диф не касается чувствительных к перфу путей.
  • Полный node scripts/mutation-gate.mjs (весь набор мутантов) — не прогонял; прогнал только «чистый прогон» гардов до первой (несвязанной) остановки и точечно проверил новый мутант вручную патчем/ребилдом.

Вердикт

Все AC1–AC4 доказаны: AC1/AC2 браузерными смоками (прогнаны лично), AC3 — мутант проверен вручную и убит новым смоком, AC4 — чтением диффа (гарды идентичны принятым образцам) плюс смок-проверка декор-гарда исполнением. Трейлеры и changelog в порядке. Находок нет.

Зелёный.