Files
houseplan-card/docs/reviews/CODE-REVIEW-533-r1.md
T
2026-09-11 16:31:13 +00:00

198 lines
16 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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.
---
<!-- material-anchors: сгенерировано конвейером (#414) -->
## Материал раунда
- Ветка: `issue/533-resize-witness-mapping`, коммит `acea69506907` — ребейз его осиротит, и это нормально: ниже якоря, которые ребейз не меняет.
- Дерево материала: `123a41ad67f07b692c0b8e6f0ff9b6da4746510e`
```
git log --all --format='%H %T' | grep 123a41ad67f0
```
- Тело issue: `ae465893153dbb7ba1b9d991e2d5cf4a05c8f526b51ae2f2b74e6e2c566805d9`
- Вердикт конвейера: `yellow` · High 0