mirror of
https://github.com/Matysh/houseplan-card
synced 2026-10-01 12:18:51 +00:00
docs: spec #316 — migration auto-resolves opening-host conflicts
Issue: #316 User-Visible: no
This commit is contained in:
@@ -0,0 +1,109 @@
|
||||
# Issue #316 — миграция v9 сама разрешает конфликт «проём ↔ нулевая стена»
|
||||
|
||||
Статус: ревизия 1. Родитель: #306 (стены нулевой толщины), контекст: #319
|
||||
(бэкенд-гард — отдельная задача). Решение владельца (чат, 2026-08-26):
|
||||
конфликт проёма при миграции разрешается автоматически, рисование нигде не
|
||||
блокируется; выпуск — v1.68.0-beta.4 вместе с #319.
|
||||
|
||||
## 1. Сценарий и пользовательский результат
|
||||
|
||||
На v1.68.0-beta.3 два клика инструментом «Стены» на пустом плане заканчиваются
|
||||
тостом «Пространство не преобразовано: проём нельзя однозначно привязать к
|
||||
стене. Данные не изменены». Причина воспроизведена исполнением: структурный
|
||||
писатель проходит атомарный барьер `commitWallSegmentModel` на весь конфиг;
|
||||
`hostRoomOpenings` требует для каждого контурного проёма ровно одного носителя
|
||||
с `cm > 0`; проём, который легаси-`open_span` перевёл бы на нулевую стену,
|
||||
кидает `WallSegmentModelError('opening-host')` — и конфликт в любом одном
|
||||
пространстве блокирует структурные записи во всех, включая пустые.
|
||||
|
||||
После фикса: первая структурная запись (и «Оптимизировать планы») мигрирует
|
||||
документ всегда; конфликтные проёмы разрешаются детерминированно по правилам
|
||||
§3; ни одно пространство не блокирует другое. Пользовательские данные проёмов
|
||||
не удаляются.
|
||||
|
||||
## 2. Границы
|
||||
|
||||
- Правила §3 действуют **только при initial migration** (`model_version < 9`
|
||||
на входе в `commitWallSegmentModel`). Пост-v9 структурные записи сохраняют
|
||||
прежний fail-closed контракт: невозможность разместить проём — ошибка данных,
|
||||
запись отклоняется целиком (гарантии #278/#282 не ослабляются).
|
||||
- §8.2 ТЗ #306 в части «Legacy-конфликт open_span/open_to с проёмом блокирует
|
||||
миграцию всего пространства» заменяется настоящим документом. Остальное
|
||||
(«новый проём на нулевой стене запрещён», «перевод участка с проёмом в 0
|
||||
отклоняется») действует без изменений — это пост-миграционные операции.
|
||||
- Бэкенд-гард stale-client не входит в скоуп (#319).
|
||||
|
||||
## 3. Нормативные правила авто-разрешения (в порядке применения)
|
||||
|
||||
### 3.1 Проём удерживает свою стену
|
||||
|
||||
Атом, несущий существующий проём (интервал проёма `[t·L − length/2,
|
||||
t·L + length/2]` пересекает интервал атома на общем носителе, с допуском
|
||||
`GRID_STEP_N * 0.02` по расстоянию и тем же угловым критерием, что в
|
||||
`resolveRoomOpeningHost`), **не переводится в `cm: 0`** легаси-катом
|
||||
(`open_spans`/`open_to`): он сохраняет толщину, которую имел бы без ката.
|
||||
Пункт 8 контракта #306 сужается: «open_spans переводят покрытые атомы в cm:0,
|
||||
кроме атомов, несущих проём». Физический смысл: если внутри «границы» стоит
|
||||
дверь — в этом месте стена была и остаётся; разрыв (нулевые атомы) продолжается
|
||||
по обе стороны от проёма.
|
||||
|
||||
### 3.2 Неоднозначный носитель
|
||||
|
||||
Если после 3.1 у проёма больше одного подходящего кандидата (`eligible` из
|
||||
`resolveRoomOpeningHost`), выбирается детерминированно: текущий `host`, если он
|
||||
среди кандидатов; иначе ближайший по расстоянию от центра проёма до оси; при
|
||||
равенстве — больший `cm`; при равенстве — лексикографически меньший `id`.
|
||||
|
||||
### 3.3 Проём без носителя
|
||||
|
||||
Если кандидатов с `cm > 0` нет вовсе: берётся ближайший по расстоянию сегмент
|
||||
пространства, удовлетворяющий угловому критерию и вместимости, без ограничения
|
||||
допуска расстояния. Если и таких нет (в пространстве нет ни одного пригодного
|
||||
сегмента) — проём сохраняется в данных **без `host`** и становится инертным:
|
||||
не создаёт тоннель/вырез/тело, не участвует в физике, рендерится по своим
|
||||
`x/y` как прежде. `hostRoomOpenings` при initial migration пропускает такой
|
||||
проём вместо исключения; пост-v9 запись, не меняющая этот проём, обязана его
|
||||
сохранять, а picker/placement позволяет переставить его на стену штатно.
|
||||
|
||||
### 3.4 Никаких исключений из миграции по проёмам
|
||||
|
||||
При initial migration `hostRoomOpenings` не кидает `opening-host` ни при каких
|
||||
данных. Прочие причины (`invalid-room`, `duplicate-id`, `zero-length`) остаются
|
||||
блокирующими — это настоящие поломки данных, а не легаси-конфликт.
|
||||
|
||||
## 4. AC
|
||||
|
||||
- **AC1 (репродукция #316).** Конфиг: пустое пространство A + пространство B с
|
||||
комнатой, `open_span` вдоль стены и дверью на той же стене. Два клика
|
||||
инструментом «Стены» в A создают черновик; тоста «Пространство не
|
||||
преобразовано» нет; запись уходит. Доказательство: смок, повторяющий
|
||||
выполненную репродукцию (до фикса красный).
|
||||
- **AC2 (3.1).** После миграции конфига B дверь остаётся на атоме с исходной
|
||||
толщиной; атомы span'а по обе стороны проёма — `cm: 0`. Доказательство:
|
||||
юнит-тест на `commitWallSegmentModel`.
|
||||
- **AC3 (3.2).** Два коллинеарных совпадающих кандидата (контурный +
|
||||
независимый, кейс #313) — host выбирается по правилу 3.2 и стабилен при
|
||||
повторном прогоне (идемпотентность: второй прогон байт-идентичен).
|
||||
Доказательство: юнит-тест.
|
||||
- **AC4 (3.3).** Проём вдали от всех стен мигрирует в состояние «без host»,
|
||||
конфиг проходит `CONFIG_SCHEMA` бэкенда, повторная запись его сохраняет.
|
||||
Доказательство: юнит-тест фронта + бэкенд-тест схемы.
|
||||
- **AC5 (граница §2).** Пост-v9 запись с проёмом, потерявшим носителя,
|
||||
по-прежнему отклоняется `opening-host` (fail-closed не ослаблен).
|
||||
Доказательство: существующий/новый юнит-тест.
|
||||
- **AC6.** Прогон миграции идемпотентен: `commit(commit(x)) == commit(x)`
|
||||
байтово на фикстурах AC1–AC4. Доказательство: юнит-тест.
|
||||
|
||||
## 5. Риски
|
||||
|
||||
- Изменение зонирования cm:0 (3.1) сдвигает границы атомов относительно
|
||||
beta.3 — на документах, уже мигрировавших на beta.3, правило 3.1 не
|
||||
сработает (span'ы уже удалены, атомы уже cm:0): такие проёмы попадут в 3.2/3.3.
|
||||
Принято: beta.3 прожила меньше суток, владелец предупреждён.
|
||||
- «Инертный» проём (3.3) — новое состояние данных; риск ограничен рендером по
|
||||
x/y и пропуском в физике; picker уже умеет переставлять проёмы.
|
||||
|
||||
## 6. Откат
|
||||
|
||||
Реверт ветки; миграция снова кидает `opening-host` (поведение beta.3). Данные,
|
||||
уже мигрировавшие с авто-разрешением, остаются валидными v9-документами.
|
||||
Reference in New Issue
Block a user