16 KiB
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 завершился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— ребейз его осиротит, и это нормально: ниже якоря, которые ребейз не меняет. - Дерево материала:
123a41ad67f07b692c0b8e6f0ff9b6da4746510egit log --all --format='%H %T' | grep 123a41ad67f0 - Тело issue:
ae465893153dbb7ba1b9d991e2d5cf4a05c8f526b51ae2f2b74e6e2c566805d9 - Вердикт конвейера:
yellow· High 0