Files
houseplan-card/docs/specs/292-resize-availability-audit.md
2026-08-24 18:11:08 +03:00

16 KiB
Raw Permalink Blame History

Issue #292 — объяснимая доступность Resize на реальном плане

  • Issue: https://github.com/Matysh/houseplan-card/issues/292
  • Статус: первая редакция для внешнего ревью; канонический статус задаётся метками issue
  • Тип / приоритет: bug / P1
  • Область: eligibility safe Resize, причины запрета, реальная regression fixture, production pointer smoke и документация ограничений
  • Модель данных: schema и model version не меняются
  • Зависит от: #276, #289, #290; активная, но инертная рукоятка исправляется в #293

1. Сценарий, персона и подтверждение

Кто и где: владелец сложного реального плана открывает Plan → Resize после импорта либо Optimize и пытается предсказуемо изменить размеры комнаты.

Момент проблемы: большинство видимых ручек приглушены. Пользователь не понимает, является ли это неисправностью плана, ограничением безопасного Resize или дефектом самой ручки, и не знает, какое условие мешает операции.

До/после: до задачи запреты выглядят одинаково и не защищены exact audit; после задачи каждая разрешимая стена активна, а настоящий запрет имеет стабильную человеческую причину без внутреннего jargon.

На test/fixtures/real-plan-second-floor.json текущий Resize показывает 37 рукояток: 11 разрешены и 26 запрещены. Текущий расклад причин запрета:

Причина resolver Рукояток Наблюдаемая геометрия
unequal-shared 7 соседняя комната имеет другие endpoints/протяжённость
partial-shared 6 сосед владеет только частью moving edge
side-angle 6 примыкающее ребро не строго перпендикулярно
duplicate-physical-wall 5 совпадающая independent geometry
diagonal 2 moving edge не осевая

Сама доля запретов не является ошибкой: unsafe resize обязан остаться запрещённым. Ошибка — ложные запреты после исправления/оптимизации геометрии, отсутствие стабильного аудита реального плана и тексты, из которых пользователь не понимает, что именно мешает перемещению стены.

2. Пользовательский результат

После попадания зависимостей и повторного Optimize каждая стена, которую можно сдвинуть без изменения topology, числа стен, физических толщин и более чем двух комнат, получает активную рукоятку. Остальные рукоятки остаются видимыми и при click, Enter или Space объясняют ограничение обычным языком и называют действие только там, где оно безусловно: например, убрать перекрывающую independent geometry. Статический текст не обещает, что Optimize исправит конкретную наклонную стену.

Внутренние коды (partial-shared, side-angle и т. п.) пользователю не показываются. Разрешённая рукоятка обязана реально двигаться; этот отдельный интеграционный контракт закрывает #293.

3. Детерминированный eligibility audit

Один pure audit проходит те же room polygons, openings, walls, open spans, partitions, drafts и columns и вызывает тот же resolveSafeResize, который использует production render. Он возвращает:

  • общее число rendered handles;
  • enabled count;
  • disabled count по каждому SafeResizeReason;
  • стабильный идентификатор handle: room id, edge index, canonical endpoints;
  • для exact shared pair — обе owner copies с одинаковым результатом.

Audit не является новой пользовательской телеметрией и не попадает в config. Его результат для обеих real-plan fixtures хранится как test expectation. После merge зависимостей implementation фиксирует новый точный baseline в том же коммите; любое последующее изменение counts требует осознанного обновления fixture expectation и объяснения в code review.

4. Правила доступности

  1. Exact horizontal/vertical outer wall с двумя perpendicular side edges и без конфликтов разрешена, если существует хотя бы один ненулевой safe grid step.
  2. Exact shared wall разрешена только при полном endpoint-to-endpoint владении ровно двух комнат и одинаковом допустимом диапазоне; обе owner-рукоятки дают один plan и один reason.
  3. Ограничение #289 для mixed-role side edge не ослабляется: сохранение одной толщины ценой перезаписи другой запрещено.
  4. Near-axis вход сначала исправляет явный Optimize #290. Сам Resize не меняет старую геометрию молча.
  5. Совпадающая independent geometry после canonical cleanup #276 не должна оставаться ложным duplicate-physical-wall; настоящая partition/draft/column продолжает блокировать.
  6. enabled разрешён только если один шаг в заявленном направлении проходит тот же structural/persistence preflight, что live preview. Resolver не может обещать операцию, которую первый pointermove гарантированно отвергнет.
  7. Нельзя разрешать resize, изменяющий больше двух комнат, vertex count/order, число физических wall records либо их cm.

5. Тексты причин

RU/EN тексты должны отвечать «что мешает», а «что можно сделать» добавляется только для безусловного действия, не требующего нового geometry analysis. Минимальный контракт:

Класс Пользовательский смысл
angled / side angle стена или примыкание не горизонтально/вертикально; Resize поддерживает только точные оси, без обещания автоматического Optimize
independent overlap поверх границы лежит отдельная перегородка, черновой контур или колонна; её нужно убрать/перенести
partial / unequal shared соседняя комната использует не всю ту же стену; безопасно двигать её как общую нельзя
multiple rooms сдвиг затронул бы более двух комнат
thickness / opening невозможно сохранить толщину либо безопасное положение проёма
invalid geometry перемещение нарушило бы структуру плана; без внутреннего кода ошибки

Динамический признак «Optimize способен исправить эту стену» и новые варианты SafeResizeReason не вводятся этой задачей. Если в будущем появится такой контракт, он оформляется отдельно и не является условием #292.

6. Scope

Входит

  • общий deterministic audit production eligibility;
  • устранение доказанных ложных запретов после #276/#290;
  • согласование owner copies exact shared wall;
  • actionable RU/EN причины и keyboard/click activation;
  • exact-count tests на обеих реальных фикстурах;
  • production-bundle smoke, который проходит UI Plan → Resize.

Не входит

  • ослабление safe-resize ограничений для partial/mixed/diagonal topology;
  • произвольный vertex editor или перемещение более двух комнат;
  • исправление pointer no-op разрешённой рукоятки (#293);
  • автоматический lossy repair при load/save;
  • пользовательская аналитика/телеметрия.

7. Acceptance criteria

AC1. Реальный baseline зафиксирован

После rebase поверх #276/#289/#290 тест запускает audit на real-plan-first-floor.json и real-plan-second-floor.json, сравнивает exact total/enabled/reason counts и печатает diff по handle id при расхождении. Expectation не допускает >=, snapshots без причин или глобальную tolerance.

AC2. Известная общая стена разрешена

Стена room-a edge 2 / room-b edge 2 второй фикстуры классифицируется как одна exact shared physical wall: обе owner copies enabled, plan содержит ровно две комнаты, endpoints и safe range совпадают. Ни одна третья комната не входит.

AC3. Ложные причины исчезают после repair

Targeted fixtures доказывают, что подтверждённый near-axis repair #290 снимает diagonal/side-angle, а canonical cleanup #276 снимает только ложный duplicate-physical-wall. Настоящие diagonal и independent partition остаются запрещены с правильной причиной.

AC4. Unsafe cases остаются закрыты

Partial shared, unequal endpoints, mixed-role side edge #289, third-room, thickness conflict и opening collision остаются disabled. Mutation, удаляющая любой guard, убивается точным reason/count либо geometry invariant.

AC5. Причина доступна в production UI

Production bundle показывает disabled handle с aria-disabled=true, доступным focus и локализованным aria-label. Click, Enter и Space показывают тот же actionable текст; pointerdown не создаёт drag и не пишет config/history.

AC6. Resolver и render не расходятся

Source/production-path guard доказывает, что audit и render используют один resolver и один reason type. Нельзя иметь отдельную упрощённую копию условий для тестового счётчика.

AC7. Первый шаг согласован с preview

Для каждого enabled handle хотя бы одно направление имеет ненулевой grid step, проходящий topology и persistence preflight. Если оба направления отвергаются, handle disabled с детерминированной причиной, а не active no-op.

AC8. Инварианты

После всех разрешённых targeted moves на real fixtures проходят wall keys, mixed-role records, references, opening host/fit, room simplicity/orientation, physical geometry preflight; число стен и multiset wall.cm не меняются.

AC9. Локальные гейты

  • npm run typecheck;
  • npm test;
  • npm run build и bundle parity;
  • node scripts/check-docs.mjs;
  • targeted eligibility audit, production smoke и mutation.

Полные golden, smoke, performance и Linux HA harness выполняются перед beta.

8. Совместимость, touch и performance

Schema/storage не меняются. Старый frontend продолжает читать тот же план. Touch editor остаётся best effort, но disabled reason доступен через tap, а активный pointer stream не конфликтует с pinch/pan/pointercancel.

Audit выполняется только в тестах. Production render сохраняет существующий snapshot cache: resolver не должен пересчитываться более одного раза на handle за frame. Performance budget safe Resize из docs/RESIZE.md не меняется.

9. Риски и меры

  • Погоня за меньшим процентом disabled может разрешить unsafe topology. Мера: точная negative matrix AC4 и structural/persistence preflight AC7–AC8.
  • Counts могут разойтись с production render из-за второй реализации audit. Мера: один resolver/reason type и source guard AC6.
  • Статический текст может пообещать repair, которого нет для true diagonal. Мера: §5 запрещает такое обещание; fixtures проверяют near-axis и настоящую диагональ раздельно.
  • Полный audit в render может ухудшить editor responsiveness. Мера: test-only audit и существующий per-snapshot eligibility cache.

10. Откат

Чистый revert implementation-коммита возвращает прежнюю eligibility и тексты; миграция/feature flag не требуются, persisted geometry/schema не меняются.

11. Ожидаемые файлы

  • src/resize.ts, src/houseplan-card.ts;
  • src/i18n/en.json, src/i18n/ru.json;
  • unit/production-path tests и exact fixture audit;
  • demo/smoke_room_resize.mjs либо отдельный real-plan smoke;
  • docs/RESIZE.md, docs/TESTING.md, при необходимости docs/ARCHITECTURE.md;
  • docs/CHANGELOG.md, docs/CHANGELOG.ru.md.

12. Release

Implementation-коммит имеет Issue: #292, User-Visible: yes и обновляет оба changelog. Issue не закрывается вручную: она закрывается выпуском beta.

13. Принятые предположения

  1. Метрика 70% — диагностический сигнал, а не продуктовая цель «разрешить любой ценой»; после исправлений число запретов может оставаться высоким.
  2. Точный post-dependency baseline фиксируется только после rebase всех трёх зависимостей, иначе тест законсервирует заведомо переходное состояние.
  3. Дублированные owner handles допустимы в DOM для hit testing, но обязаны иметь одинаковый resolution; audit явно показывает physical equivalence.