28 KiB
CODE-REVIEW-337-r2
- Issue: https://github.com/Matysh/houseplan-card/issues/337
- Материал:
git diff 6d338b78..HEAD(r1 SHA6d338b78a0794491639edee9171662cdd260c42b→ HEAD2beafc06398c16d1cad80fddbe77e6a40f111b9a), веткаissue/337-lazy-editor-chunk. 4 коммита, 32 файла (без учёта сгенерированныхdist/*/custom_components/houseplan/frontend/*, которые зеркалят исходники):src/houseplan-card.ts(+79/-13),src/houseplan-editor-runtime.ts(+55/-55, преимущественно механическая правка),scripts/mutation-gate.mjs, два unit-теста, 11 browser-smoke фикстур, документ CODE-REVIEW-337-r1, обновлённыйdocs/images/screenshots.json. - Этап: код-ревью, заход r2, блокирующих циклов израсходовано 1/4 до этого разбора.
- Вердикт: красный.
Скоуп разбора
Предыдущий вердикт (CODE-REVIEW-337-r1, SHA 6d338b78) — красный, 5 High + 3 Medium.
Дельта этого раунда — точечные правки по каждой находке, а не переработка задачи:
два изменённых src-файла, обновлённые smoke-фикстуры под новый async-контракт,
одна правка mutation-gate (стал ссылаться на актуальный файл стилей после переноса
CSS), плюс сам документ r1 и пересборка bandle-артефактов. Это не ребейз на ушедший
dev (branch и так уже стоит на 508945c0 = origin/dev), не смена контракта
поведения и не новая подсистема — разбор веду по дельте, а не с нуля, но
дополнительно перепрогоняю весь browser-smoke census и golden:verify, а не
только смоки, названные автором: H4-фикс (_deviceInboxTabKey/@click=${this._x}
→ @click=${() => this._x()}) механически переписал практически КАЖДЫЙ
обработчик события в houseplan-editor-runtime.ts (не только вкладки инвентаря
устройств) — это затрагивает общий рендер-путь всех редакторских диалогов, а не
только те четыре сценария, что назвал автор.
Как проверялось
Дешёвые гейты (перепрогнаны самостоятельно, а не приняты со слов автора):
| Гейт | Команда | Результат |
|---|---|---|
| typecheck | npx tsc --noEmit |
pass (5.6s) |
| unit | npm test |
1439 total, 1438 pass, 1 skip, 0 fail — совпадает с заявлением автора |
| build+sync+budget | npm run build && npm run bundle:sync && npm run bundle:budget |
initial View 255 778 B ≤ 256 000 B (запас 222 B — уже, чем 615 B на r1); lazy editor 131 785 B; bundle:sync не оставил расхождений в дереве (только биты прав доступа 755→644 на двух артефактах из-за локального tsc, отменено git checkout --, содержимого не касалось) |
| docs fingerprint | node scripts/check-docs.mjs (diff трогает src/**) |
pass (7 файлов, 10 внешних ссылок) |
| mutation, changed-range | node scripts/mutation-gate.mjs --check --changed=6d338b78..HEAD |
58/58 применимых guard'ов поймали подмену |
Тяжёлые гейты — по дельте и по решению ревьюера, не выборочно на веру:
- Полный browser-smoke census (195 файлов,
for f in demo/smoke_*.mjs; do node "$f"; doneпослеbundle:sync) — 195/195 pass, включаяsmoke_warm_dialogs(единственный прогон в составе census). Это ожидаемо и НЕ противоречит находке H6 ниже: точечный прогон того же файла 7 раз подряд дал 5 отказов из 7 (≈71% красных) — при однократном прогоне в общем census благополучный исход (≈29% по моей выборке) не редкость. Единичный зелёный census — статистический шум для гонки такой частоты, а не доказательство исправности; полагаться на однократный прогон здесь означало бы повторить саму ошибку хендоффа автора. npm run golden:verify— 127 passed / 4 different — те же четыре кадра, что в r1:geometry-devices-editor-dark,device-dialog-mobile-ru,toggle-entity-dialog-mobile-ru,device-ripple-color-popover-mobile-ru.- Независимая проверка авторского заявления по H5 (не принято на веру: автор
утверждает «pre-existing stale baseline, не регресс #337» — это фактическое
утверждение о состоянии
dev, которое легко проверить и легко сфальсифицировать). Поднялgit worktree add /tmp/dev-check origin/dev --detach(чистый508945c0, ancestor этой ветки),npm ci && npm run build && npm run bundle:sync, затемnode demo/golden/run.mjs --mode=capture --scenario=<id>(verify требует полную матрицу, capture позволяет точечный diff) для всех четырёх кадров: все четыре «different» воспроизводятся на чистомdevбез единого изменения из #337. Открыл сами diff-PNG:geometry-devices-editor-dark— актуальный рендер показывает два раздельных, не наложенных друг на друга элемента тулбара («Devices», «Icon rules»); диффовое наложение возникает потому что эталон старше и содержит три кнопки («Add», «Hidden and disabled», «Icon rules») — устаревший baselinedev, а не порча layout веткой. Три mobile-ru кадра показывают одинаковую узкую полосу по правому/нижнему краю диалога и на чистомdev— тоже baseline drift, не регресс. H5 закрыт: находка воспроизводится наdevи не относится к #337; это отдельный, предсуществующий дефект золотых эталонов — завёл отдельный issue (#346). - Целевые smoke, привязанные к находкам r1, каждый прогнан минимум по
разу, некоторые — многократно при подозрении на нестабильность (ниже):
smoke_preloader_lifecycle,smoke_warm_owners,smoke_kiosk,smoke_room_resize,smoke_optimize_geometry_preflight,smoke_device_inbox,smoke_fixed_floor,smoke_orphan_space_references,smoke_general_settings,smoke_ha_controls,smoke_nav_persist,smoke_partition_openings,smoke_grid_scale_invariance,smoke_edit_walk,smoke_help_affordance,smoke_opening_entity_search. smoke_warm_dialogs— прогнан 7 раз подряд послеbundle:sync, а не один раз. Причина повторов: первый запуск далbViewBitExactFAIL на кадре, близком, но не идентичном сохранённой камере — при плавающей точке одного разового отказа недостаточно, чтобы отличить гонку от единичной аномалии окружения. Результат: 5 из 7 запусков красные, см. находку H6 ниже. Это прямо противоречит заявлению автора «warm_dialogs green» в хендоффе.- model-invariants — не запускал: дельта не меняет ни один формат хранения
геометрии (
layout,marker.space,open_spans, ключи толщины); H3-фикс меняет только то, ЧЕРЕЗ КАКОЙ объект (thisvsthis.host) вызывается уже существующая preflight-проверка, не сами данные. Не тот инструмент для этой дельты — то же обоснование, что в r1. - Backend/pytest — не тронуты этой дельтой (
custom_components/**/*.pyне входит в diff6d338b78..HEAD), не перепрогонял.
Закрытие раунда r1
| Находка r1 | Чем закрыта | Где видно |
|---|---|---|
H1 (warm-remount коммитит mode в обход _ensureEditorRuntime()) |
_warmAdoptViewport больше не коммитит редакторский mode напрямую: адаптирует 'view' синхронно, а восстановление mode уходит через _requestMode(restoredMode, false, true), который проходит _ensureEditorRuntime(); новый параметр adopt в _requestMode атомарно вызывает _adoptMode + возобновляет отложенный _warmReviveDialog |
src/houseplan-card.ts:890-901 (сигнатура/adopt-ветка _requestMode), :3190-3222 (_warmAdoptViewport), :3358-3369 (_warmReviveDialog откладывает потребление диалога, если ждёт runtime) |
| H2 (kiosk scale зависит от lazy runtime) | _saveKioskScale/_renderKioskDialog возвращены в eager-код хоста, больше не форвардятся в _editorRuntimeOrThrow(); вызов диалога больше не защищён условием this._editorRuntime ? |
src/houseplan-card.ts:10629-10662, :11350 (${this._kioskDialog ? this._renderKioskDialog() : nothing}) |
| H3 (fail-closed resize/optimize preflight не перехватывается подменой) | Внутренние вызовы _checkSpacePhysicalGeometry/_checkOptimizeGeometry внутри runtime переведены на this.host._checkSpacePhysicalGeometry(...)/this.host._checkOptimizeGeometry(...) — то есть идут через тот же публичный метод хоста, который переопределяет смок; сами реализации переименованы в ..._Impl и не вызываются напрямую нигде, кроме хоста |
src/houseplan-editor-runtime.ts:1117-1120 (интерфейс HouseplanEditorHostPort пополнен), :1761, 2085, 2156, 2265, 2288, 3609, 3626, 8591, 8599, 8688, 8721 (все внутренние вызовы через this.host.), src/houseplan-card.ts:9973/9984 (_checkOptimizeGeometry/_checkSpacePhysicalGeometry хоста форвардят в ..._Impl) |
H4 (_deviceInboxTabKey падает — this не тот) |
Метод стал полем-стрелкой (public _deviceInboxTabKey = (event) => {...}), а КАЖДЫЙ голый @event=${this._method} в файле переписан в @event=${() => this._method(...)} — то есть устранён весь класс бага, а не только один обработчик; закреплено регрессионным unit-тестом, который grep'ает файл на голые @x=${this._y} и требует пустой список |
src/houseplan-editor-runtime.ts:7401-7410 (метод), десятки мест по файлу (см. git diff 6d338b78..HEAD -- src/houseplan-editor-runtime.ts), test/editor-runtime-loader.test.mjs:124-133 («lazy runtime event handlers keep the runtime receiver») |
| H5 (4 golden-дельты, включая порчу тулбара Device editor) | Не изменено ни строки, относящейся к golden — не регресс #337: independently подтверждено, что все 4 кадра дают «different» и на чистом origin/dev (508945c0) без единого коммита этой ветки, см. раздел «Как проверялось» |
git diff origin/dev...HEAD -- demo/golden/baselines/ пуст (нулевой diff эталонов); скриншот-diff geometry-devices-editor-dark на чистом dev показывает то же наложение старого 3-кнопочного baseline поверх нового 2-кнопочного актуального рендера |
M1 (fixed_floor/orphan_space_references создают houseplan-card-editor напрямую) |
Обе фикстуры переведены на await customElements.get('houseplan-card').getConfigElement() |
demo/smoke_fixed_floor.mjs:75, demo/smoke_orphan_space_references.mjs:191 |
M2 (general_settings ищет версию только в entry-файле) |
Смок теперь читает houseplan-assets.json, конкатенирует все .js-файлы манифеста и ищет баннер в объединённой строке |
demo/smoke_general_settings.mjs:61-67 |
| M3 (9 нерасследованных красных смоков) | ha_controls, nav_persist, partition_openings, grid_scale_invariance явно адаптированы под async editor runtime (_editorRuntime, await c._requestMode(...), await c._ensureEditorRuntime()) |
demo/smoke_ha_controls.mjs, demo/smoke_nav_persist.mjs, demo/smoke_partition_openings.mjs, demo/smoke_grid_scale_invariance.mjs (везде в диффе 6d338b78..HEAD) |
Новые находки
High (блокирует)
H6. Warm-remount editor-камера теряет бит-точность — визуальный «прыжок» кадра, воспроизводится в большинстве прогонов.
Контракт (комментарий самого смока, demo/smoke_warm_dialogs.mjs:1-9,
docs/WARM-REMOUNT.md): при пересоздании элемента картой (Lovelace warm
remount) камера редактора должна восстановиться бит-в-бит — ни один кадр не
отличается от прежнего. Прогнал node demo/smoke_warm_dialogs.mjs 7 раз
подряд после npm run bundle:sync: 5 из 7 — FAIL на bViewBitExact:
FAILED (1):
- bViewBitExact: expected true, got "кадр 20мс: zoom=3.4
view=[256.7064599980745,247.64705882352942,346.587080003851,264.70588235294116]
(ждали 3.4 / [254.1156927887434,247.64705882352942,351.7686144225132,264.70588235294116])"
Во всех пяти отказов кадр приходит на 9–20мс после пересоздания, zoom и y/h
точно совпадают с ожиданием, а x/w — нет (расхождение 2–6 логических
единиц) — то есть не полный сброс камеры, а короткий одиночный кадр с
пересчитанной шириной вида, прежде чем всё встаёт на место. Это ровно та зона,
что переписал H1-фикс: _warmAdoptViewport (src/houseplan-card.ts:3196-3222)
синхронно восстанавливает _zoom/_view в точные сохранённые числа ДО того,
как _mode меняется на 'devices', но сам _mode первый кадр или два остаётся
'view' (полноширинная сцена без редакторского тулбара), а асинхронный
_requestMode(restoredMode, false, true) переключает _mode только после
_ensureEditorRuntime(). Между этими двумя моментами существует окно, где
камера уже стоит в «devices»-числах, но сцена ещё рендерится в View-раскладке
(другая доступная ширина stage) либо наоборот — и какой-то путь размера/layout
(вероятно, привязанный к ResizeObserver/первому измерению _stageEl для
нового инстанса, _lastValidStageSize пуст на свежесозданном элементе)
пересчитывает x/w под несовпадающую раскладку на один кадр.
Смок умеет падать — это не переписанный этой задачей файл, докстрока прямо говорит «ПАДАЕТ на сборке до DEV-B703-03» (задача, которая изначально ввела это требование), и я лично наблюдал детерминированный, воспроизводимый по большинству прогонов красный результат, а не разовую аномалию: 5/7. Полный census (195/195, единичный прогон каждого файла) этот файл не поймал — статистически ожидаемо при частоте отказа 71% и единственной попытке (см. раздел «Как проверялось»), а не признак исправности.
Это отдельная находка от H1 (не то же самое, что было в r1): в r1 весь смок падал с необработанным исключением ДО того, как код доходил до этой проверки — H1-фикс устранил исключение, но обнажил под ним ещё один, ранее не наблюдаемый дефект в том же коде. Прямо противоречит хендоффу автора («warm_dialogs... green», перечислено как pass в списке «targeted review smokes»). Учитывая majority-red частоту (71%) и то, что визуальный прыжок камеры при warm-remount — это ровно тот UX-дефект, ради предотвращения которого существует весь контракт DEV-B703-03/WARM-REMOUNT.md, серьёзность максимальная.
Medium (вне скоупа #337 — отдельный issue)
M4 (→ #346). Golden-эталоны
geometry-devices-editor-dark, device-dialog-mobile-ru,
toggle-entity-dialog-mobile-ru, device-ripple-color-popover-mobile-ru
устарели на origin/dev независимо от #337. Воспроизведено на чистом
508945c0: node demo/golden/run.mjs --mode=capture --scenario=<id> для
каждого из четырёх даёт different; diff-PNG geometry-devices-editor-dark
показывает наложение устаревшего 3-кнопочного тулбара эталона на актуальный
2-кнопочный рендер (тулбар Device editor был упрощён каким-то более ранним,
уже смёрженным в dev изменением, после которого эталон не переснимали); три
mobile-ru кадра — устаревший размер диалога по правому/нижнему краю. Нужно
принять новые эталоны через npm run golden:accept -- --reviewed по чистому
dev, не через эту ветку. Issue заведён.
Что проверено и корректно
- Бюджет (AC1). 255 778 B ≤ 256 000 B, посчитано лично. Запас упал с 615 B
(r1) до 222 B — риск, названный ещё в r1, материализуется: правки H1/H4
добавили код (новый параметр
_requestMode, обёртки обработчиков), и запас почти исчерпан. Не блокирую этим, так как AC1 формально выполнен и риск уже зафиксирован, но следующая правка (в том числе исправление H6) обязана держать бюджет в уме. - H2/H3/H4/M1/M2 фактически исправлены — не только по диффу, но по собственному прогону соответствующих smoke, см. таблицу выше.
- H5 закрыт корректно и добросовестно — авторское расследование не просто
заявлено, а воспроизведено мной независимо на чистом
dev. - Мутационное покрытие затронутых guard'ов (changed-range) — 58/58 ловят
подмену; changed-range корректно расширился под новый файл
src/styles/plan.styles.ts, где теперь реально лежит проверяемый CSS. - Полный browser-smoke census — 195/195 pass на единичном прогоне (не противоречит H6, см. выше).
- Trailers/changelog. Все 4 коммита несут
Issue: #337; все —User-Visible: no— корректно, т.к. пользовательское поведение (после исправления H1–H4) возвращено к состоянию ДО дефектов r1, changelog для #337 уже писался отдельным коммитом на предыдущем этапе (c1aaddc7, зафиксирован в r1).
Унаследовано из r1
Принято без повторной проверки в этом раунде, дельта их не касается:
- AC7 (backend asset route) —
tests_backend/test_frontend_assets.py, не входит в diff6d338b78..HEAD; вывод r1 (CODE-REVIEW-337-r1.md, SHA6d338b78, раздел «Что проверено и корректно») остаётся в силе. - AC8 (bundle sync/freshness/HACS zip/release scripts) — не входит в дельту; вывод r1 наследуется.
- AC9 (CSS-минификатор, adversarial fixtures) — не тронут этой дельтой,
кроме одной ссылки в
mutation-gate.mjsна новый путь файла стилей, что не меняет сам минификатор; вывод r1 наследуется. - AC10 (fingerprint mismatch), AC12 (no model drift), AC13 (докс не обещает single-file install) — вне дельты, наследуются из r1 документа.
- Прочие смоки, не входящие в таблицу закрытия r1, включая полный набор 195 файлов — пройдены заново единичным census + точечными повторными прогонами там, где я подозревал гонку, а не приняты по r1 без проверки: дельта тронула общий рендер-путь всех редакторских диалогов (H4-правка), поэтому я не ограничился списком автора.
Чего не проверял
- Полный HA-harness backend (
python -m pytest tests_backend -q) — недоступен в этой песочнице (нетhomeassistant), дельта не трогаетcustom_components/**/*.py, поэтому и не требовался в этом раунде. node scripts/model-invariants.mjs/npm run invariants— не запускал, дельта не меняет формат геометрической записи (обоснование выше).- Perf-профили — не запускал, не названы в AC и не затронуты дельтой.
- Ручное тестирование в реальном Home Assistant — не выполнялось.
- Частоту гонки H6 в CI-окружении (headless Linux CI vs моя песочница) — не измерял отдельно; предполагаю сравнимую или более высокую частоту (CI раннеры обычно медленнее/более вариативны по таймингу кадров, что скорее усилит гонку, чем скроет её), но это предположение, а не измерение.
Итог
Три из пяти High r1 закрыты полностью (H2, H3, H4), H5 закрыт как посторонний
(и это подтверждено, а не принято на слово), но H1 закрыт лишь частично:
устранённый крах открыл дорогу новому, статистически частому (5 из 7 прогонов)
дефекту в той же самой warm-remount/lazy-adopt зоне — H6, видимый прыжок
камеры редактора при пересоздании карточки. Это блокирующая находка, вердикт
остаётся красным. Заявление автора «warm_dialogs... green» не подтвердилось
при независимом многократном прогоне — рекомендую на следующем заходе
прогонять чувствительные к таймингу smoke (smoke_warm_dialogs,
smoke_warm_owners, smoke_preloader_lifecycle) не менее 3–5 раз подряд
перед хендоффом, а не один раз.
Отдельно: заведён новый issue #346 на предсуществующий (не из #337) дефект золотых эталонов Device editor/mobile-ru диалогов (M4) — он не блокирует #337 и не должен чиниться в этой ветке.