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

241 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.
# 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.