# CODE-REVIEW — issue #533, заход r1 **Материал:** `acea695069075fb1bd7592f3f2b38853bc9f4c32` (единственный коммит поверх `origin/dev`, ветка `issue/533-resize-witness-mapping`, приведена к `dev` конвейером: `97666f9b -> acea6950`, +2 коммита dev). Разбор полный (после ребейза — другой код, §7.2), но это первый заход ревью, поэтому разделы «Закрытие раунда» и «Унаследовано» не применяются. **Диапазон:** `git diff origin/dev...HEAD --stat` → один файл, `demo/smoke_room_resize.mjs` (+69/-64). Продуктовый код не тронут — класс B (гейты и инструментарий), трейлеры коммита: `Issue: #533`, `User-Visible: no` — соответствует (чейнджлог не требуется). ## Скоуп Трек `trivial`. Свидетель `demo/smoke_room_resize.mjs` считал экранные точки жеста один раз, заранее, через `svg.getScreenCTM()`, а карточка переводит их обратно в момент события — при рассинхронизации раскладки между замером и жестом (раннер против локальной машины) резайз коммитил не ту величину, и чистый прогон падал четырьмя немыми `expected true, got false`. Три AC из тела issue: - **AC1** — точка каждого события берётся непосредственно перед отправкой (не один раз заранее); `pointermove`/`pointerup` целятся в ту же ручку, что и `pointerdown`; смок падает, если событие не доставлено. - **AC2** — новая проверка `safe_resize.mapping_stable`: масштаб `getScreenCTM` на `pointerdown` и `pointermove` совпадает в пределах 1e-6, с обоими масштабами и размерами стейджа в сообщении. - **AC3** — смок зелёный локально и на раннере на материале кандидата; штатный мутант `safe-resize-commit-preflight-bypassed` по-прежнему ловится. ## Как проверялось **Прогнал сам:** - `node demo/smoke_room_resize.mjs` локально (после `npm run build` + `npm run bundle:sync`, свежий бандл): **зелёный**, `OK`, 0 провалов; вывод подтверждает `mapping_stable` (`scale` совпадает на `down`/`move`) и правильный `safeResizeResult` (`[450, 450, 450, 0.45]`). - Эксперимент на «умеет ли тест падать» для найденного пробела (см. находку ниже): временно испортил цель `pointermove` в сценарии `mixed_role` (`cx: 999, cy: 999` — заведомо не существующая ручка), прогнал смок повторно — он остался зелёным (`OK`), хотя событие физически не было доставлено. Файл восстановлен из бэкапа сразу после эксперимента, `git status` — чисто. - `node scripts/smoke-select.mjs --base origin/dev --head HEAD` → «Исполняемого frontend-диффа нет (`src/**/*.ts` не тронут). Browser-smoke этим диффом не выбираются — выбирать нечего». **Не прогонял, и почему:** - `npx tsc --noEmit`, `npm test`, `npm run build` (сверка бандла), `node scripts/check-docs.mjs` — дёшевые гейты уже зелёные на этом SHA: Validate [run 34621275434](https://github.com/Matysh/houseplan-card/actions/runs/34621275434) завершился `success`, job «Переиспользование: это дерево уже проверено» — `success`, `check-docs` не применим — diff не трогает `src/**`. - `npm run invariants` — diff не трогает геометрию, `layout`, `marker.space`, `open_spans`, толщины стен: правок продуктового кода нет вообще. - `npm run golden:verify` — diff не может изменить рендер (только текст свидетеля). - `python -m pytest tests_backend -q` — `custom_components/**/*.py` не тронут. - performance-профили — не названы в AC, чувствительные пути не тронуты. - Полный набор `demo/smoke_*.mjs` — не запускал; `smoke-select` явно сказал «выбирать нечего» (нет диффа в `src/**`), а AC называют только `smoke_room_resize.mjs`, который я прогнал напрямую. Дополнительно проверил на самом SHA `acea6950` через `gh api .../commits/.../check-runs`: все шесть шардов «Мутанты по диффу» (включая «Мутанты по диффу (2/6)» — именно тот шард, что падал в исходном инциденте) — **success**. Это прямое подтверждение AC3 на раннере: этот job сначала гоняет чистый прогон затронутых свидетелей, потом мутанты по ним — зелёный исход означает и «чистый прогон больше не мигает», и «мутант по-прежнему ловится» одновременно. ## Разбор по AC **AC1 — частично.** Перевод координат теперь происходит внутри того же `page.evaluate`, что и отправка события (`pointer()`, строки 63–75) — единый кадр для всех 22 вызовов в файле, `screenPt` удалён полностью. Целевая ручка: во всех местах, где раньше `pointermove`/`pointerup` вызывались без `cx/cy` (искали «первую незаблокированную ручку»), теперь передаётся то же `cx/cy`, что и в `pointerdown` — проверил построчно все 8 сценариев (главный, `disabled_no_drag`, `mixed_role`, `owner_boundary`, `corner_clamped`, `preflight`, `commit_preflight`, `cancel`) — везде консистентно. Отдельно проверил по коду (`_rszRooms()` → `houseplan-editor-runtime.ts:3250`), что ручки рендерятся от персистентной модели (`space.rooms`), а не от live-превью резайза, — значит `cx/cy` ручки не плывёт в процессе перетаскивания и поиск по исходным координатам остаётся корректным на всём жесте (проверено чтением, не исполнением). Требование «смок падает, если событие не было доставлено» выполнено только для **трёх** из 22 вызовов `pointer()` — обёрнуты в `sent()` только `down_sent`/`move_sent`/`up_sent` главного сценария (строки 129, 131, 143). Остальные 19 вызовов (`disabled_no_drag`, все три события `mixed_role`, `owner_boundary`, `corner_clamped`, все четыре `preflight`, оба `commit_preflight`, все три `cancel`) по-прежнему отбрасывают возвращаемое значение — тот же паттерн, который AC1 прямо называет дефектом. Экспериментом подтвердил: подмена `cx/cy` на заведомо несуществующую ручку в `mixed_role` (`pointermove`) делает событие недоставленным, но смок остаётся зелёным. Это не гипотетический риск — именно так текущая версия способна молча пропустить регресс доставки событий в любом из этих 7 сценариев, ровно то, от чего должен защищать AC1. **AC2 — выполнено.** `safe_resize.mapping_stable` (строки 132–137) сравнивает `grab.scale` и `moved.scale` с допуском `1e-6`, имя проверки содержит оба масштаба и оба размера стейджа. Локальный прогон подтверждает: `scale` идентичен на `down`/`move` (`0.6144444290587165` в обоих), проверка проходит. **AC3 — выполнено.** Локально три прогона (де-факто больше — при отладке находки) стабильно зелёные. На раннере: все шесть шардов «Мутанты по диффу» на `acea6950` зелёные, включая ранее падавший 2/6 — свидетельство того, что раннер больше не мигает и что мутанты (в т.ч. `safe-resize-commit-preflight-bypassed`) по-прежнему отлавливаются. ## Находки ### Medium (в скоупе задачи) — AC1 закрыт частично: 19 из 22 вызовов `pointer()` не проверяют доставку события **Файл:** `demo/smoke_room_resize.mjs`, сценарии `mixed_role` (244–246), `owner_boundary` (263–265), `corner_clamped` (292–294), `preflight` (315–323), `commit_preflight` (342, 352), `cancel` (368–370), `disabled_no_drag` (212). **Симптом:** только `down_sent`/`move_sent`/`up_sent` в главном сценарии (129, 131, 143) оборачивают `pointer()` в `sent()`. Остальные 19 вызовов по всему файлу игнорируют `result.sent`, как это делал старый код. **Воспроизведение:** временно заменил `cx: 100, cy: 928` на `cx: 999, cy: 999` во втором `pointer('pointermove', …)` сценария `mixed_role` (строка 245) — целевая ручка перестаёт находиться, `dispatchEvent` не вызывается (`target?.dispatchEvent` с `target === undefined`). Смок тем не менее выводит `OK` без единого провала: последующие проверки (`safe_resize.mixed_role_no_drag`, `safe_resize.mixed_role_geometry_exact`) случайно совпадают с «жест не состоялся», потому что drag и так не должен двигать геометрию в этом сценарии. Изменение отменено сразу после проверки, рабочее дерево чистое. **Почему это в скоупе, а не отдельный issue:** AC1 сформулирован в issue как общее требование к свидетелю («смок падает, если событие не было доставлено»), без ограничения одним сценарием, и правка находится в том же файле и в той же задаче. Это ровно тот класс дефекта, который спровоцировал инцидент (немой булев результат вместо явного провала) — в оставшихся семи сценариях `pointerdown`/`pointermove`/`pointerup` может провалиться молча, и тест этого не заметит. Цена исправления — обернуть оставшиеся вызовы в `sent()`, как уже сделано для главного сценария. ## Что проверено и корректно - Перевод координат «в момент события» — for всех 22 вызовов, `screenPt` удалён без остатка (`grep` по репозиторию не находит других мест использования). - Консистентность цели `pointermove`/`pointerup` с `pointerdown` — проверено построчно по всем 8 сценариям. - `mapping_stable` — реализация и допуск (`1e-6`) соответствуют AC2, имя проверки печатает диагностику, как требуется. - Ручки не двигаются во время live-резайза (рендерятся от персистентной модели, не от превью) — значит поиск ручки по исходным `cx/cy` безопасен на протяжении всего жеста, включая многошаговые сценарии. - `nudge` — корректно реализованная замена прежнего «второй `pointermove` на чуть другую точку»: теперь это явно тот же план-таргет плюс пиксельный дребезг, что точнее соответствует намерению проверки `preflight_reason_once` (дребезг указателя не должен размножать toast). - Удаление поля `safeResizePoints` из `finish()` — не используется больше нигде в репозитории, безопасно. - Трейлеры коммита корректны: `Issue: #533`, `User-Visible: no`, чейнджлоги не тронуты и не требуются (класс B, без пользовательского эффекта). - Продуктовый код не тронут ни единой строкой — соответствует «Вне объёма» из ТЗ. ## Чего не проверял и почему Список дан выше, в разделе «Как проверялось» → «Не прогонял, и почему»: typecheck/test/build/check-docs (уже зелёные на этом SHA, дешёвые гейты), инварианты модели, golden, pytest, перф-профили, полный набор смоков — по всем diff не даёт оснований их гонять. ## Вердикт Находка Medium — в скоупе задачи, блокирующего High нет. По PROCESS.md §7.2 это жёлтый вердикт: правка возвращается автору для дозакрытия AC1 (обернуть оставшиеся 19 вызовов `pointer()` в `sent()` тем же способом, что и три уже обёрнутых), без создания отдельного issue. --- ## Материал раунда - Ветка: `issue/533-resize-witness-mapping`, коммит `acea69506907` — ребейз его осиротит, и это нормально: ниже якоря, которые ребейз не меняет. - Дерево материала: `123a41ad67f07b692c0b8e6f0ff9b6da4746510e` ``` git log --all --format='%H %T' | grep 123a41ad67f0 ``` - Тело issue: `ae465893153dbb7ba1b9d991e2d5cf4a05c8f526b51ae2f2b74e6e2c566805d9` - Вердикт конвейера: `yellow` · High 0