mirror of
https://github.com/Matysh/houseplan-card
synced 2026-09-30 11:49:16 +00:00
241 lines
16 KiB
Markdown
241 lines
16 KiB
Markdown
# 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.
|