18 KiB
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 — OKsmoke_vacuum.mjs— назван AC2 в issue напрямую (нет в выдаче smoke-select, т.к. не ссылается на изменённые символы текстом, но AC требует его явно) — OKsmoke_vacuum_firstuse.mjs— назван AC2, плюс прямое совпадение на_vacCalConfirm(гард К2) — OKsmoke_decor.mjs— прямое совпадение на_decorShapeDown, метод реально изменён (К2-гард) — OKsmoke_drag_bounds.mjs— то же самое совпадение на_decorShapeDown— OKsmoke_cold_view_toggle.mjs— тот же класс регрессии (#357), родственный контракт «холодная вкладка +_editorRuntime» — OKsmoke_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 в порядке. Находок нет.
Зелёный.