Files
houseplan-card/docs/specs/293-resize-pointer-noop.md
2026-08-24 17:35:11 +03:00

15 KiB
Raw Permalink Blame History

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 на конфигурации, эквивалентной реальному приложению.