# Issue #293 — активная рукоятка Resize обязана выполнять pointer-жест - **Issue:** https://github.com/Matysh/houseplan-card/issues/293 - **Статус:** первая редакция для внешнего ревью; канонический статус задаётся метками issue - **Тип / приоритет:** bug / P1 - **Область:** production pointer pipeline safe Resize, live preview transaction, pointer capture, commit/cancel/Undo и browser regression smoke - **Модель данных:** schema и model version не меняются - **Связано:** #277, #289, #292; правила eligibility не расширяются ## 1. Подтверждённый дефект На `test/fixtures/real-plan-second-floor.json` общая стена `room-a` edge 2 / `room-b` edge 2 имеет одинаковые endpoints и разрешена `resolveSafeResize`. Обе рукоятки активны. `pointerdown` создаёт корректный plan для двух комнат, но настоящие `pointermove` на 2–60 px не дают видимого preview, а `pointerup` не меняет geometry/history. Чистая математика на том же plan исправна: `validateSafeResize`, `clampSafeResize` и `applySafeResize` принимают ненулевые шаги в обе стороны и изменяют только две комнаты. Значит, дефект находится в production integration между pointer event и live overlay/commit. Текущее наблюдение `d=0, moved=false` не доказывает, что ошибка только в screen-to-SVG conversion: `_rszMove` также молча возвращает старое `d`, если `_rszApplyPreview` отвергает persistence metadata. Реализация обязана сначала зафиксировать точную причину отказа на полноценно инициализированной карточке, а затем исправить её. Маскировать ошибку прямым вызовом private methods нельзя. ## 2. Пользовательский результат Если рукоятка выглядит активной, drag по нормали стены немедленно показывает live preview. Общая стена и обе комнаты движутся синхронно; стены, проёмы, virtual spans и thickness records следуют за своей геометрией. Pointerup делает ровно один commit и один Undo-шаг. Если runtime preflight всё же обнаруживает конфликт, рукоятка не может молча остаться активной и инертной: предсказуемый конфликт должен быть отражён в eligibility до жеста, а непредсказуемый отказ во время жеста останавливает preview на последней безопасной позиции и один раз показывает локализованную причину. Нулевая позиция без объяснения запрещена. ## 3. Pointer contract 1. `pointerdown` на enabled handle сохраняет immutable geometry snapshot, `pointerId`, plan, options и начальную точку в SVG coordinates. 2. Последующие события того же pointer вычисляют signed displacement как проекцию **дельты от начальной pointer point** на `plan.n`; место click вдоль самой стены не влияет на результат. 3. Новая ось стены проходит grid snap один раз, затем `clampSafeResize`. 4. Gesture получает pointer capture на `currentTarget` handle либо эквивалентный общий listener, поэтому продолжает работать за пределами круга. 5. Pointer другого id, hover с `buttons=0`, pinch/pan и unrelated stage gesture не изменяют resize drag. 6. `pointerup` коммитит только уже показанный валидный preview. `pointercancel`, `lostpointercapture` до нормального release и Esc отменяют overlay без записи. 7. Один physical shared wall может иметь две owner circles, но started gesture привязан к одному immutable plan и не переключается на circle после rerender. ## 4. Live overlay contract Preview строится из immutable pre-drag geometry и записывается только в `_rszPreview`; `_serverCfg` до pointerup не меняется. На каждом candidate: - room polygons и openings берутся из одного `applySafeResize` result; - wall/open-span rekey выполняется из исходных spans, не из предыдущего frame; - число wall/open-span records и multiset `wall.cm` сохраняются; - общий physical geometry preflight проверяет exact overlay; - reject возвращает структурированную причину, а не просто `false`. Предсказуемые metadata/preflight условия должны использоваться и eligibility #292. Непредсказуемый reject не сбрасывает успешно показанный preview. При первом кандидате он оставляет `moved=false`, но показывает локализованный toast; повтор того же reject в рамках жеста не спамит уведомления. ## 5. Реальный browser smoke Regression test загружает production bundle и настоящую tracked fixture через полноценный `_serverCfg`/space path. Он входит в Plan через штатный mode path, выбирает Resize и отправляет browser pointer events по фактическим screen coordinates handle. Тесту запрещено вызывать `_rszMove`, `_rszApplyPreview`, `applySafeResize` или менять rooms после начала жеста. Private state можно читать только как дополнительную диагностику; pass/fail основан на DOM preview, persisted config, history и invariants. Smoke обязан сначала доказать, что fixture space действительно является текущим объектом `_serverCfg.spaces`, чтобы неполный setup не выдавал ложный no-op. ## 6. Scope ### Входит - точная локализация и исправление enabled pointer no-op; - pointer origin/capture и устойчивость к rerender; - структурированный reject live preview и пользовательская обратная связь; - реальный fixture browser smoke на 10 grid steps и 60 px; - cancel, commit, Undo, metadata и geometry invariants; - RU/EN i18n для runtime reject, если существующего текста недостаточно. ### Не входит - изменение правил, какие стены считаются eligible (#292/#289/#290); - разрешение partial-shared, mixed-role, diagonal или >2-room Resize; - новый resize UX, новая кнопка подтверждения или новый режим; - изменения schema/storage; - исправление геометрии real fixture через прямую мутацию теста. ## 7. Acceptance criteria ### AC1. Реальный enabled drag работает Production-bundle smoke на `room-a` edge 2 / `room-b` edge 2 выполняет pointerdown, движение ровно на 10 grid steps и pointerup. До release DOM/live geometry показывает 10 шагов, persisted geometry неизменна; после release обе комнаты изменены ровно на 10 шагов, shared endpoints совпадают. ### AC2. Движение за пределами handle не теряется Отдельный drag проходит не менее 60 screen px по нормали. Все intermediate events одного pointer доходят до жеста, preview монотонно следует safe clamp. Mutation, удаляющая capture/common listener, убивается этим smoke. ### AC3. Pointer delta не зависит от click вдоль стены Три старта — midpoint и точки у обоих концов hit area — при одинаковой дельте по нормали дают один `d`. Тангенциальное движение даёт ноль; reversed owner copy даёт тот же physical target, а не противоположный результат. ### AC4. Один commit и Undo Pointerup после ненулевого preview создаёт одну history command и одну config write. Undo восстанавливает byte-equivalent pre-drag room/opening/wall/open-span geometry. Synthesized click не меняет selection и не создаёт второй commit. ### AC5. Esc и interruption безопасны Esc, pointercancel и lost capture до release удаляют overlay, оставляют config и history неизменными и не запускают отложенную запись. Событие другого `pointerId` игнорируется. ### AC6. Preview reject не молчит Targeted test заставляет persistence/preflight отвергнуть первый candidate. Geometry не меняется, `_serverCfg` не мутируется, пользователь получает ровно один локализованный toast. Если reject детерминирован до pointerdown, handle disabled через #292 вместо runtime toast. ### AC7. Metadata и topology сохраняются После реального drag проходят room simplicity/orientation, shared ownership, wall keys, mixed-role records, opening host/fit и physical render preflight. Vertex counts/order, число wall/open-span records и multiset `wall.cm` неизменны; затронуты не более двух комнат. ### AC8. Harness не может дать ложную зелень Smoke падает, если fixture не находится в текущем `_serverCfg`, handle disabled, preview отсутствует, persisted config меняется до release или движение сделано private method. Mutation, возвращающая `d` к нулю либо заставляющая `_rszApplyPreview` отвергать candidate, убивает smoke. ### AC9. Существующие сценарии не регрессируют Outer wall, synthetic exact pair, clamp у угла, disabled handle, opening move, preflight failure и cancellation в `smoke_room_resize` остаются зелёными. ### AC10. Локальные гейты - `npm run typecheck`; - `npm test`; - `npm run build` и bundle parity; - `node scripts/check-docs.mjs`; - targeted real-plan pointer smoke и mutation. Полные golden, smoke, performance и Linux HA harness выполняются перед beta. ## 8. Совместимость, touch, security и performance Schema/storage не меняются. Desktop mouse является обязательным контрактом. Touch editor остаётся best effort, но safety floor обязателен: pinch/pan и pointercancel не коммитят resize, а active single-pointer drag не теряется. Новых HA actions/security boundaries нет. Pointermove сохраняет существующий bounded `clampSafeResize` и snapshot cache; нельзя сериализовать config или пересчитывать весь model сверх текущего preview path более одного раза на candidate. Safe Resize performance budget из `docs/RESIZE.md` сохраняется. ## 9. Риски и меры - Точная причина интеграционного no-op устанавливается только на этапе реализации: screen-to-SVG conversion и молчаливый reject live-preview дают одинаковый внешний симптом. AC1, AC6 и AC8 требуют реальный pointer smoke и mutation для обоих путей, поэтому частичное исправление не пройдёт. - Pointer capture и обработка отмены могут задеть touch safety floor. AC2 и AC5 отдельно закрепляют выход за handle, `pointercancel`, lost capture и чужой `pointerId`, а AC9 сохраняет существующие сценарии. - Исправление live-preview может случайно ослабить safe Resize. AC7 оставляет topology, metadata и physical preflight обязательными и ограничивает изменение двумя комнатами. ## 10. Откат Откат — полный revert implementation-коммита #293 вместе с тестами и документацией. Schema, model version и сохранённые данные не меняются, отдельная миграция или восстановление конфигурации не требуются. ## 11. Ожидаемые файлы - `src/houseplan-card.ts`, при необходимости `src/resize.ts`; - `src/i18n/en.json`, `src/i18n/ru.json`, если добавляется runtime reason; - `demo/smoke_room_resize.mjs` либо отдельный real-plan pointer smoke; - unit/production-path/mutation tests; - `docs/RESIZE.md`, `docs/TESTING.md`, при необходимости `docs/ARCHITECTURE.md`; - `docs/CHANGELOG.md`, `docs/CHANGELOG.ru.md`. ## 12. Release Implementation-коммит имеет `Issue: #293`, `User-Visible: yes` и обновляет оба changelog. Issue не закрывается вручную: она закрывается выпуском beta. ## 13. Принятые предположения 1. Скриншот владельца и реальная fixture имеют приоритет над ограничением первоначального программно собранного стенда; новый smoke обязан устранить это различие setup. 2. Structured preview reject — часть внутреннего API, а не новый persisted формат. 3. Если root cause окажется только в неполной инициализации старого smoke, это не считается исправлением пользовательского бага без воспроизведения и зелёного pointer path на конфигурации, эквивалентной реальному приложению.