From 5b7d37e911db0e9b664475d3fda647345513f48e Mon Sep 17 00:00:00 2001 From: Codex Date: Wed, 26 Aug 2026 18:00:32 +0300 Subject: [PATCH] =?UTF-8?q?docs:=20spec=20#316=20=E2=80=94=20migration=20a?= =?UTF-8?q?uto-resolves=20opening-host=20conflicts?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Issue: #316 User-Visible: no --- .../specs/316-opening-host-auto-resolution.md | 109 ++++++++++++++++++ 1 file changed, 109 insertions(+) create mode 100644 docs/specs/316-opening-host-auto-resolution.md diff --git a/docs/specs/316-opening-host-auto-resolution.md b/docs/specs/316-opening-host-auto-resolution.md new file mode 100644 index 00000000..8f763e51 --- /dev/null +++ b/docs/specs/316-opening-host-auto-resolution.md @@ -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-документами.