Files
houseplan-card/docs/reviews/CODE-REVIEW-579-r1.md
2026-09-14 20:00:07 +00:00

19 KiB
Raw Permalink Blame History

CODE-REVIEW-579-r1

Issue: #579 — «WebView pinch compositor churn» Материал: 216990e469f2d123a81c03e99cdcb315ed756be0 (диапазон origin/dev..HEAD, один продуктовый коммит 216990e4 + служебный 0cbc7aff документ ревью ТЗ) Заход: r1 (первый код-ревью; ТЗ ревьюилось отдельно и получило зелёный вердикт, см. docs/reviews/SPEC-REVIEW-579-r1.md) Блокирующих циклов израсходовано: 0 из 4

Скоуп

Задача убирает churn промотирования/демотирования сцены ([data-hp-live-viewbox="camera"|"floor"]) во время активного pinch/pan: раньше каждый budget-refresh (100 мс / 15% сдвига) и любой полный Lit-кадр во время активного жеста переписывали viewBox и одновременно снимали transform/transform-origin/will-change/temporary overflow, а следующий промежуточный кадр создавал слой заново. Это совпадает с полевым дефектом HA Companion WebView (мигание/ белые кадры стен и пола при pinch).

Изменённые файлы класса A: src/live-viewport.ts, src/live-interaction-runtime.ts. Классы B/C/D — тесты, документация и синхронизированный бандл, все в том же коммите. Геометрия, жесты, i18n, конфигурация не затронуты (совпадает с заявленным не-скоупом ТЗ).

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

Полный разбор (не по дельте): это первый код-ревью, отдельного r0 не было.

Дешёвые гейты подтверждены зелёным Validate на этом SHA (run 34887568637 / 34887188635 — оба на 216990e4, typecheck+unit+build+bundle-sync+no-new-any и diff-мутанты все success); heavy-джобы (smoke, golden, backend, hacs, hassfest, geometry_parity, performance_smoke) там ожидаемо skipped — обычный push, не relaese-кандидат. Дополнительно к зачёту CI я лично прогнал на этом дереве:

Гейт Команда Результат
Typecheck+unit+build+bundle-sync подтверждено Validate (см. выше) зелёный, не перегонял
no-new-any node scripts/no-new-any.mjs --base origin/dev --head HEAD новых any нет (31 строка в 2 файлах)
check-docs node scripts/check-docs.mjs 7 файлов, 12 внешних ссылок — зелёный
unit npm test npm test 2707 pass / 0 fail / 1 skip (сошлось с хендоффом)
unit #579 node --test --test-name-pattern="#579" test/live-viewport.test.mjs 1/1 зелёный
смок, выбранный smoke-select node scripts/smoke-select.mjs --base origin/dev --head HEAD → 1 «зарегистрированная связь»: demo/smoke_live_pan_coverage.mjs см. ниже
demo/smoke_live_pan_coverage.mjs прогнал напрямую OK, все именованные проверки true, включая новые stablePromotion*
demo/smoke_editor_gestures.mjs AC8 OK, все проверки true
demo/smoke_smooth_zoom.mjs AC8 OK
demo/smoke_tap_ctx.mjs AC8 OK
demo/smoke_long_press_gesture.mjs AC8 ok:true
demo/smoke_isometric_live_touch.mjs второй consumer live-viewport OK, все проверки true
npm run continuity:screencast AC3, единственный оракул на представленные (не screenshot) кадры OK — capturedPinchPresentedFrames: true, noEmptyOrBlackPresentedFrame: true
npm run golden:verify диф трогает live-viewport рендер-путь все сценарии passed, осевших кадров byte-diff нет
Мутант live-pinch-compositor-demoted-on-budget-refresh node scripts/mutation-gate.mjs --id=live-pinch-compositor-demoted-on-budget-refresh «заявленный тест покраснел на мутанте», поймано 1/1
Мутант live-pinch-compositor-demoted-on-active-lit-commit node scripts/mutation-gate.mjs --id=live-pinch-compositor-demoted-on-active-lit-commit «заявленный тест покраснел на мутанте», поймано 1/1

Не прогонял:

  • Полный набор demo/smoke_*.mjs (249 штук) — задача не задевает всё дерево, smoke-select вернул одну зарегистрированную связь плюс явно названные в AC8 смоки; этого требует диф.
  • python -m pytest tests_backend — диф не касается custom_components/**/*.py.
  • npm run model-invariants — геометрия и ссылки на неё не менялись.
  • Performance-профиль как гейт (пороговое сравнение с базовым SHA) — по ТЗ и PROCESS §8 это предрелizный, а не ревью-гейт (AC9 сам требует «профиль перед бетой», не перед ревью); я прогнал npm run benchmark:large-house-interaction вручную как дымовую проверку (скрипт отработал, разумные числа, без падений), но это не заменяет сравнение с бюджетом — честно фиксирую как непройденный гейт, а не как «проверено».
  • Физический прогон на HA Companion Android (AC10) — недоступен в этом окружении; по ТЗ и по прежнему прецеденту (CODE-REVIEW-544-r1) это полевая приёмка перед бетой, не перед код-ревью, и автор честно отметил её как «НЕ сделано» в хендоффе.

AC → доказательство · чем краснеет

AC Доказано Чем краснеет
AC1 стабильная promotion-сессия unit test/live-viewport.test.mjs (#579 a live session keeps one promoted scene layer...), я прогнал мутант live-pinch-compositor-demoted-on-budget-refresh — красный, поймано 1/1 (я прогнал)
AC2 бюджет сохранён тот же unit-тест: root.camera.writes === viewBoxWritesBeforeRefresh + 1 через порог/15%; browser smoke stablePromotionKeepsViewBoxBudget (я прогнал, true) naive возврат к записи viewBox на каждый кадр красит этот assert — проверено чтением теста, отдельного именованного мутанта на этот AC нет (Low, см. ниже)
AC3 сцена не пропадает в представленных кадрах npm run continuity:screencast, я прогнал: noEmptyOrBlackPresentedFrame:true, capturedPinchPresentedFrames:true заложенный в скрипт forbidden-фильтр по всей серии кадров, включая pinch-участок; отдельного мутанта нет — оракул сам по себе экспериментальный (пиксельный)
AC4 эффекты не отключаются (day cycle/Glow/room fills/hatching/фон) не доказано отдельным тестом — см. находку M1 —
AC5 слои синхронны ≤1px demo/smoke_live_pan_coverage.mjs (isoMouseRightEdgeMarkerParity и др., pre-existing, не тронуто #579), я прогнал — зелёный —
AC6 terminal cleanup unit-тест (settled removes all styles) + smoke stablePromotionCleansAtTerminal, я прогнал мутант live-pinch-compositor-demoted-on-active-lit-commit — красный, поймано 1/1 (я прогнал)
AC7 существующее покрытие края не потеряно demo/smoke_live_pan_coverage.mjs coverage-проверки, я прогнал — зелёный существующий (до #579) mutant этого смока входит в diff-мутанты CI (6/6 success) — не прогонял лично, доверяю CI
AC8 нет регрессии управления smoke_editor_gestures, smoke_smooth_zoom, smoke_tap_ctx, smoke_long_press_gesture, я прогнал все — зелёные — (не защитный AC в терминах §2.7, обычное сравнение)
AC9 производительность не прогнан как гейт (см. «Не прогонял») предрелизный, по ТЗ и PROCESS §8
AC10 полевой Companion-контроль явно не сделано, задокументировано в хендоффе приёмка перед бетой, не перед код-ревью

Находки

Medium (в скоупе задачи — жёлтый вердикт, чинится в этом issue)

M1 — AC4 (матрица day cycle/Glow/room fills/hatching/фон) не имеет тестового доказательства.

ТЗ (раздел «Критерии приёмки», AC4) прямо требует: «Матрица day cycle off/on, Glow off/on, room fills, штриховка и пользовательский фон остаётся видимой во всех sampled кадрах. Доказательство: браузерный smoke на контрастной fixture». Диф трогает три тестовых файла: test/live-viewport.test.mjs, demo/smoke_live_pan_coverage.mjs, demo/screencast_visual_continuity.mjs. Ни один не включает Glow (glow_enabled/glow_radius_cm) или day cycle в используемой fixture — проверено чтением: дефолтный fixture demo/srv/demo.html (const CFG = {... settings: {} ...}) не задаёт ни то, ни другое, а обе смоки не трогают card._serverCfg.settings вообще (grep по обоим файлам на glow/day_cycle — пусто). Единственная fixture-правка — уже существовавшая до #579 «coverage-room» с fill_color, не новая для этой задачи.

Воспроизведение находки (не дефект продукта — пробел evidence): grep -n "day_cycle\|glow" demo/smoke_live_pan_coverage.mjs demo/screencast_visual_continuity.mjs — 0 совпадений.

Смягчающее обстоятельство, найденное чтением src/houseplan-card.ts: room fills, Glow (_renderGlowBaseRooms/_renderGlowLayer, строки ~11769–11777) и hatching рендерятся внутри того же корневого <svg data-hp-live-viewbox="floor"> (открывается на :11593), что и стены/пол — то есть тот же самый механизм compositor-стабильности, который AC1 доказывает для стен, структурно распространяется и на них: нет отдельного код-пути, который мог бы демотировать Glow независимо от пола. renderDayCycleEnvironment (:11583), напротив, рендерится ДО этого SVG как отдельный элемент без data-hp-live-viewbox — он вообще не участвует в исправляемом lifecycle, поэтому риск для дневного цикла из этого бага структурно ниже, чем для Glow. Это разобрано чтением, а не исполнением, и не заменяет обещанный в ТЗ смок.

Остаточный риск нельзя закрыть только чтением: Glow использует SVG-фильтры/blur-пулы, а именно комбинации GPU-фильтров с компоновкой слоёв — типичный источник WebView-специфичных артефактов (сам issue ссылается на это как на общий класс проблем). Обещанный оракул («sampled кадры» во время удержанного pinch с Glow/day-cycle включённым) технически достижим тем же приёмом, что уже применён в runStablePromotion/screencast_visual_continuity — включить glow_enabled/day-cycle в используемую fixture и подтвердить отсутствие потери пикселей в held-кадрах, как уже делается для coverage-room.

Без блокирующих (High) находок это жёлтый вердикт: правка (добавить Glow/day-cycle в существующую fixture смоков и подтвердить сохранение видимости в held-кадрах, либо явно и обоснованно снять эту часть AC4 через ревью) остаётся в этом же issue, отдельный issue не заводится (#202).

Low

L1 — AC2 не имеет именованного мутанта. У AC2 («бюджет сохранён») нет специального MUTANT_DEFINITIONS-контрпримера — доказательство ограничено прямым assert'ом в unit-тесте (root.camera.writes === viewBoxWritesBeforeRefresh + 1), которого я прочитал и прогнал; он действительно упал бы при регрессии к побюджетной записи, но без явной мутации это не формализовано как «чем краснеет» по правилам §2.7. AC2 не новый в этой задаче (природа budget refresh — из #531), находка образовательная, не блокирует: снимаю с записью, отдельной правки не требую.

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

  • Единственная точка входа commitHouseplanViewport(host, keepSceneLayer) имеет ровно один вызывающий (LiveRuntime.commit() → this.active()), никаких других колл-сайтов с устаревшей сигнатурой не осталось (grep подтверждает).
  • this.active() покрывает оба независимых источника continuous-режима: реальные pointer- контакты (_pointers.size, drag-состояния) и анимированные камера-переходы (_cameraTransition.active, которым идут wheel-zoom через _startCameraTransition и toolbar-zoom) — прочитано _onWheel/_stepZoom, оба используют camera-transition, а не прямую мутацию _zoom/_view в обход контроллера, поэтому «session properties, не user- agent» (контракт п.9) выполняется единообразно для мыши, touch и программной камеры.
  • _stagePointerUp/_stagePointerCancel (не тронуты диффом, но задействованы контрактом п.4) гарантированно вызывают requestUpdate() при обнулении _pointers, что даёт терминальный Lit-кадр и, следовательно, commit(..., false) после отпускания.
  • HTML-маркеры ([data-hp-live-layer="camera"]) не получили keepSceneLayer-исключения — их путь (через painted/current, а не через якорь сцены) не изменился, что и объясняет, почему AC5 (парity ≤1px) остался зелёным без единой правки в этой части.
  • Дедупликация записи стилей (сравнение по свойству, а не строкой transform целиком) строго сильнее старой версии: старый код мог оставить transform без transformOrigin/will-change в вырожденном случае (ранний return), новый — нет; это не регрессия, а попутное усиление.
  • Изометрия использует общий sceneCamera/sceneFloor на все data-hp-live-viewbox="camera" элементы, но все они делят один и тот же view (проверено чтением шаблона :11588-11843) — риск «одна матрица на разные проекции», названный в ТЗ, не реализуется, и это поведение не менялось диффом.
  • docs/ARCHITECTURE.md, docs/TESTING.md, docs/TOUCH-SUPPORT.md обновлены точно и согласованно с кодом; docs/images/screenshots.json — только новый sourceFingerprint, байты PNG (imageSha256) не изменились (путь docs:accept --identical). Оба changelog (docs/CHANGELOG.md, docs/CHANGELOG.ru.md) в том же коммите, трейлеры Issue: #579 / User-Visible: yes на месте, ветка и коммит — один продуктовый коммит.
  • «Одно число — один источник»: диф не добавляет и не меняет ни одной пользовательски видимой величины (zoom-бейдж не тронут) — неприменимо.

Вердикт

Единственная находка — Medium в скоупе задачи (AC4 без прямого теста), блокирующих (High) находок нет. Это жёлтый вердикт: возврат автору на добавление evidence для AC4 (или обоснованное сужение этой части AC ревьюером/владельцем), остальное подтверждено чтением и исполнением перечисленных гейтов.


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

  • Ветка: issue/579-webview-pinch-compositor, коммит 216990e469f2 — ребейз его осиротит, и это нормально: ниже якоря, которые ребейз не меняет.
  • Дерево материала: 09aab067582821f39940f59986820362cf079666
    git log --all --format='%H %T' | grep 09aab0675828
    
  • Тело issue: 4545e4010a0a0d13588d41c73912f543b83f5091653de15127390b77630498a0
  • Вердикт конвейера: yellow · High 0