Files
houseplan-card/docs/specs/306-zero-thickness-walls.md
T
2026-08-26 13:39:52 +03:00

727 lines
49 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.
# #306 — Нулевые стены вместо виртуальных границ
- **Issue:** [#306](https://github.com/Matysh/houseplan-card/issues/306)
- **Статус документа:** принятый контракт; рабочий статус хранится в метках issue
- **Приоритет / тип:** P1 / feature
- **Целевая версия модели:** `PLAN_MODEL_VERSION = 9`
- **Пользовательское изменение:** да
## 1. Сценарий
Администратор дома создаёт или исправляет планировку в Plan editor на десктопе.
В момент рисования новой стены, изменения толщины существующего участка или
настройки пространства ему нужно обозначить границу без физической толщины, не
переключаясь на отдельный инструмент с другой логикой.
Задача закрывает J4 и J6 из `docs/SCOPE.md`: первоначальная настройка становится
понятнее, а последующее обслуживание плана использует одну систему стен без
рассинхронизации площади, света и геометрии.
## 2. Что человек увидит до и после
**До:** граница без физической стены создаётся отдельным неочевидным инструментом
и ведёт себя иначе обычной стены. **После:** пользователь рисует и меняет её как
обычную стену с толщиной 0, а в настройках пространства выбирает, показывать её
пунктиром с пропусканием света или сплошной линией с блокировкой света.
## 3. Цель
Убрать из продукта отдельный инструмент **«Граница»** и канонические сущности
`space.open_spans` / `rooms[].open_to`. Их роль выполняют обычные атомарные
отрезки стен с явным `cm: 0`.
В редакторе остаётся одна модель:
- `cm > 0` — стена с физическим телом;
- `cm = 0` — топологическая стена без физического тела.
После #282 каждый contour atom уже хранится в authoritative-каталоге
`space.wall_segments[]` со стабильным `id`, точными `a/b` и явным `cm`.
Происхождение нулевого атома не сохраняется и не влияет на результат: прежняя
физическая ось без тела, бывшая виртуальная граница и новая стена, нарисованная
с толщиной 0, становятся одной и той же сущностью.
Пользователь рисует и изменяет оба вида инструментами **«Стены»** и
**«Толщина»**. Настройка пространства определяет, показываются ли все нулевые
стены пунктиром или сплошной линией и пропускают ли они свет.
## 4. Зафиксированные продуктовые решения
1. Кнопка, сессия и термин **«Граница»** удаляются из UI.
2. Толщина обычной стены и перегородки принимает `0..100 см`; колонны сохраняют
диапазон `1..150 см`.
3. `0` — сохранённое значение, не пустое поле и не команда удаления записи.
4. Стиль нулевых стен задаётся один раз на пространство:
| Значение | Вид | Glow | Солнечные лучи | Общая граница комнат |
|---|---|---|---|---|
| `dashed` | пунктир | проходит | проходят | соединяет световые зоны |
| `solid` | сплошная линия | блокируется | блокируются | не соединяет световые зоны |
5. Отсутствующее/неизвестное значение настройки читается как `dashed`.
Это правило применяется ко **всем** `cm:0`, включая атомы, которые #282
создала из старых физических осей без толстого тела. В результате часть
старых планов после обновления неизбежно изменит вид или светопроницаемость;
это принятое владельцем поведение, а не ошибка миграции.
6. Нулевая стена в обоих режимах остаётся частью топологии, но не создаёт
физическое тело, бумагу, тоннель проёма и вычет площади.
7. Новый проём на нулевой стене запрещён. Перевод участка с проёмом в `0`
отклоняется до записи; проём автоматически не удаляется и не деактивируется.
8. Legacy `open_spans`, а при их отсутствии legacy `open_to`, переводят
покрытые атомы v8 в `cm:0`; все остальные уже существующие `cm:0` остаются
неотличимыми от них. Простое чтение конфигурации ничего не записывает.
9. После канонической записи старые поля больше не пишутся. Downgrade на версию,
не понимающую `cm:0`, не поддерживается; восстановление выполняется из backup.
10. Plan editor остаётся desktop-first. View и kiosk на touch входят в
блокирующий acceptance floor.
Открытых продуктовых вопросов нет.
## 5. Термины и инварианты
### 5.1 Атом стены
Атом — максимальный коллинеарный интервал с одними и теми же:
- координатами носителя;
- ролью `outer(room)`, `shared(roomA,roomB)` либо `independent(id)`;
- множеством владельцев;
- толщиной `cm`;
- границами, заданными узлами, концами существующих записей и проёмов.
Слияние соседних атомов разрешено только при полном совпадении роли, владельцев,
носителя и `cm`. Нельзя сливать `outer(A)` с `shared(A,B)`, разные пары shared,
room-wall с independent wall или пересекать узел/проём.
### 5.2 Authoritative wall identity from v8; current model v9
#282 уже реализовала Stage 1 модели стен:
- `space.wall_segments[]` содержит **каждый** contour atom и является
authoritative для identity и толщины;
- `rooms[].wall_ids[]` ссылается на эти атомы в порядке обхода `poly`;
- `space.walls[]` — только compatibility-проекция положительных толщин и не
является источником истины;
- `partitions[]` и `room_drafts[].segments[]` сохраняют собственные стабильные
ID и явный `cm`.
#306 не создаёт второй каталог и не возвращает midpoint-key identity. Она
расширяет уже существующий v8-инвариант: `cm:0` получает единый пользовательский
смысл и диапазон записи `0..100` на всех segment-based write paths. Функции
физического тела фильтруют `cm > 0`; функции топологии и редактора сохраняют и
видят `cm = 0`. При замыкании draft нулевой сегмент наследует ID по lineage-
правилам #282.
Никаких `zero_kind`, `legacy_origin`, скрытых compatibility-флагов или эвристик
по происхождению сегмента не добавляется. Два `cm:0` с одинаковой ролью и
геометрией всегда эквивалентны.
### 5.3 Единый resolver
Новый pure-модуль `src/zero-walls.ts` (имя можно уточнить без изменения
контракта) является единственной точкой для:
- `resolveZeroWallMode(space): { style, transmitsLight }`;
- проекции legacy `open_spans/open_to` на authoritative v8 atoms;
- классификации явных нулевых атомов по роли;
- получения линий световых барьеров и derived room connectivity;
- миграции v8 candidate в канонический v9 document без смены stable ID там,
где carrier не разрезается.
Glow и солнце не имеют собственных проверок `dashed/solid`. Оба используют
результат этого resolver. Визуальный renderer получает тот же `style`, но не
определяет световое поведение через CSS.
## 6. Persisted schema
### 6.1 Новое поле пространства
```ts
type ZeroWallStyle = 'dashed' | 'solid';
interface SpaceConfig {
zero_wall_style?: ZeroWallStyle; // missing/unknown read fallback = dashed
wall_segments: Array<{ id: string; a: number[]; b: number[]; cm: number }>;
walls?: Array<{ key: string; cm: number; a: number[]; b: number[] }>;
room_drafts?: Array<{ segments: Array<{ id: string; cm: number }> }>;
partitions?: Array<{ id: string; a: number[]; b: number[]; cm: number }>;
}
```
Backend принимает `cm: 0..100` для `wall_segments[]`,
`room_drafts[].segments[]` и `partitions[]`; `wall_columns[].cm` остаётся
`1..150`. Compatibility `walls[]` генерируется только из `cm>0`, всегда имеет
точные `a/b` и никогда не содержит zero record.
`PLAN_MODEL_VERSION` повышается с `8` до `9` только при фактической канонической
записи хотя бы одного затронутого пространства. Открытие карточки и
неструктурные изменения версию не повышают.
### 6.2 Deprecated compatibility fields
`space.open_spans` и `rooms[].open_to`:
- временно принимаются backend/import reader для старых документов;
- сохраняются без мутации при read-only загрузке;
- проецируются в runtime поверх v8 catalog: покрытые интервалы имеют `cm:0`, а
остальные существующие `cm:0` не получают отдельного происхождения;
- удаляются из затронутого пространства одной транзакцией после успешной
канонической миграции;
- никогда не создаются новым frontend;
- в экспорте канонического v9 отсутствуют;
- регистрируются в `docs/CONFIG-COMPATIBILITY.md` и
`scripts/config-field-registry.mjs` существующими статусами
`deprecated-read` для compatibility-read и `migrate-on-write` для
документированных structural-write/Optimize/import путей.
Если одновременно есть валидные `open_spans` и `open_to`, источником геометрии
являются `open_spans`; `open_to` не расширяет их. Если `open_spans` отсутствует
или пуст, `open_to` материализует полный доказанный общий интервал каждой пары.
Unknown sibling-поля сохраняются на read/write как сейчас.
## 7. UX редактора
### 7.1 Основная панель
- Кнопка **«Граница»**, её submenu/context tray, подсказки и состояния удаляются.
- Поле толщины у **«Стен»** принимает локализованный эквивалент `0..100 см`.
- Пустое, `NaN`, отрицательное или больше максимума значение блокирует начало
сегмента и показывает существующий range-feedback с новым диапазоном.
- `0` рисует обычный сегмент текущей wall/draft/partition цепочки. Замыкание
комнаты, Ctrl+click и Shift 45° работают без отдельной ветки.
- Pointer cancel, pinch и уход из редактора не записывают незавершённый нулевой
сегмент.
### 7.2 Инструмент «Толщина»
- Диалог/поле принимает `0..100 см` для room wall, draft и partition.
- `positive → 0` атомизирует только выбранный доказанный интервал и сохраняет
толщину соседних остатков.
- `0 → positive` восстанавливает физическое тело обычным путём.
- Пустое поле не означает `0`: Save неактивен/операция отклонена.
- Если выбранный интервал пересекает размещённый проём, `positive → 0`
атомарно отклоняется с сообщением удалить проём прежде. Остальной план не
меняется.
### 7.3 Рисование по существующей оси
Рисование `cm:0` поверх доказанного участка room wall изменяет/атомизирует
authoritative `wall_segments[]` через identity barrier #282. Оно не создаёт
совпадающую `partition`. Повторное рисование того же интервала — semantic no-op
и не добавляет Undo-команду.
На совпадающей independent partition меняется существующий объект только если
есть ровно один однозначный carrier. T/X-узел, несколько carrier или конфликт
ролей отклоняются до write с локализованным сообщением. Частичный кандидат не
сохраняется.
### 7.4 Настройки пространства
В секции отображения стен добавляется radio/select:
- заголовок RU: **«Стены нулевой толщины»**;
- варианты RU: **«Пунктирные»**, **«Сплошные»**;
- видимое пояснение RU: **«Пунктирные стены пропускают свет. Сплошные стены
блокируют свет, даже при нулевой толщине.»**;
- эквивалентные EN-строки перечислены в §15.
Изменение применяется сразу ко всем нулевым стенам пространства одной config
транзакцией, поддерживает обычную отмену несохранённого диалога и не переписывает
геометрию.
## 8. Геометрия и операции
### 8.1 Физическое тело, площадь и пол
- `cm:0` никогда не передаётся в wall-body union как полигон и не заменяется
внутренним epsilon/`1 см`.
- Чистая площадь комнаты и inset floor игнорируют его физическую толщину, но
продолжают использовать ось как контур комнаты.
- Independent `cm:0` не создаёт бумагу/пол и не вычитает площадь.
- Замкнутая цепочка independent нулевых стен не создаёт комнату сама по себе;
комната возникает только через существующий flow предложения комнаты.
### 8.2 Проёмы
- Picker/placement не предлагает нулевой wall interval как host.
- Изменение host wall в `0` запрещено, пока любой door/window/gate/passage
пересекает целевой атом.
- У нулевой стены нет tunnel fill, откосов и opening cut.
- Legacy-конфликт `open_span/open_to` с проёмом блокирует миграцию всего
пространства. Runtime остаётся читаемым по legacy projection; Optimize и
структурная запись показывают конкретный blocker и не удаляют проём.
### 8.3 Split, Merge, Delete, Resize
- Split переносит `cm:0` на соответствующий новый room-wall atom без подстановки
fallback thickness.
- Merge удаляет только доказанно внутренние дубли; нулевые атомы внешнего
контура и независимые нулевые стены сохраняются.
- Delete room с сохранением стен не превращает `cm:0` в partition с положительной
толщиной. Нулевой exclusive interval либо сохраняется как `partition cm:0`,
если пользователь выбрал сохранить стены, либо удаляется вместе с комнатой.
- Resize меняет координаты carrier, но сохраняет `cm:0`, роль и breakpoints;
примкнувшие independent стены по прежнему автоматически не двигаются.
- Каждая геометрическая операция остаётся одной именованной geometry-history
командой. Undo/Redo восстанавливает одновременно геометрию, `cm` и legacy-поля,
если миграция входила в эту транзакцию. `zero_wall_style` следует текущему
контракту Save/Cancel настроек пространства и не добавляется в geometry stack.
### 8.4 Нормализация и Optimize
- Identity writer #282 сохраняет stable ID и явные нули; соседние атомы
объединяются только при совпадении role/owners/cm и по документированным
lineage-правилам (survivor ID детерминирован, ссылки opening остаются валидны).
- Ни один helper не должен применять `clampWallCm(0) → 1`.
- Optimize preview отдельно считает `legacy virtual spans migrated` и
`zero-wall atoms merged`.
- После Confirm все пространства мигрируют атомарно; ошибка/лимит в одном
пространстве отменяет весь candidate и оставляет one-deep Optimize Undo.
- Повторный Optimize канонического v9 — byte/semantic no-op.
## 9. Свет и отображение
### 9.1 Свет
Для `dashed` нулевые сегменты исключаются из Glow/sun barriers. Shared нулевой
атом добавляет пару комнат в derived light connectivity. Для `solid` тот же
сегмент добавляется как **осевая line barrier без площади**, а shared atom не
соединяет комнаты.
Допустим вычислительный epsilon только внутри line-intersection predicate для
численной устойчивости. Он не попадает в wall body, floor, area, renderer,
opening depth или persisted data и не зависит от `cell_cm` как фиктивная
физическая толщина.
Смена `zero_wall_style` инвалидирует room connectivity, light regions, Glow
barriers, sun barriers, wall/render fingerprints и isometric cache. Она не
зависит от HA state ticks и не требует reload.
Outer и independent `cm:0` следуют тому же правилу barrier: dashed не блокирует,
solid блокирует пересечение луча. Нулевая outer wall сама не создаёт источник
солнечного света — источник по-прежнему возникает только из валидного окна.
### 9.2 Flat, isometric и редакторы
- Flat View, kiosk, isometric View и визуальный export используют выбранный
solid/dashed стиль.
- Нулевая стена рисуется одной line primitive на оси, без hatch/body/end cap.
- При `show_borders:false` нулевые стены скрыты в View/kiosk, но их выбранная
световая семантика сохраняется. Редакторы продолжают показывать их.
- Plan editor показывает служебные оси и узлы единым overlay во всех режимах;
overlay не зависит от solid/dashed.
- Decor editor применяет к нулевым стенам ту же контекстную прозрачность, что к
остальной планировке.
- Hover/hit target остаётся доступным минимумом текущего wall editor и не
ограничивается визуальной толщиной линии.
## 10. Алгоритм миграции v8 → v9
Миграция pure, deterministic, idempotent и возвращает либо полный candidate,
либо typed blocker. Документ старее v8 сначала проходит уже реализованный
identity barrier #282 и только затем этот алгоритм; отдельный второй каталог
стен не строится.
1. Валидировать v8 `rooms[].poly/wall_ids`, `wall_segments`, openings, limits и
координаты без изменения input. Для pre-v8 input получить полный v8 candidate
существующим `commitWallSegmentModel()`.
2. Если есть валидные `open_spans`, использовать их. Иначе построить полные
shared intervals по `open_to` через существующий `sharedBoundary` resolver.
3. Нормализовать и clip legacy spans строго к доказанным contour carriers.
Висящий, неоднозначный или non-finite span — blocker, не silently drop.
4. Убедиться, что endpoints каждого legacy span являются границами атомов.
Если v8 catalog уже атомизирован #282, stable IDs сохраняются. Если требуется
дополнительный split, применяется lineage #282: один доказанный survivor
сохраняет исходный ID, остальные получают новые ID, а `room.wall_ids` и
wall-hosted references обновляются атомарно.
5. Каждый атом, покрытый legacy virtual span, получает `cm:0`. Положительный
остаток сохраняет точный `cm`. Любой атом, который уже имел `cm:0`, остаётся
нулевым без отдельной метки происхождения и получает тот же style/light mode.
6. Проверить проёмы на **всех** итоговых `cm:0`, а не только на бывших virtual
spans. Zero opening host — blocker всего пространства: opening и исходный v8
документ сохраняются, молчаливое удаление запрещено.
7. Role-aware normalization объединяет только эквивалентные соседние нулевые
атомы и сохраняет валидную identity/host lineage.
8. Перегенерировать compatibility `walls[]` только из `cm>0`. Удалить
`space.open_spans` и `rooms[].open_to` только из полностью готового candidate.
9. Не добавлять `zero_wall_style`, если пользователь его не сохранял: runtime
fallback `dashed` применяется ко всем `cm:0`. Это намеренно может изменить
вид и свет старых bodyless-физических осей после обновления.
10. Прогнать geometry preflight, model invariants и backend schema. При
превышении `MAX_WALL_SEGMENTS` (сейчас 200 000) или любого другого
актуального backend-лимита отказать целиком; усечение запрещено.
11. При записи установить `model_version: 9`. Повторный запуск возвращает no-op.
### 10.1 Когда выполняется write
- Read adapter всегда строит runtime projection без persistence.
- Структурная транзакция конкретного пространства (изменение rooms, walls,
drafts, partitions, openings, Split/Merge/Delete/Resize/Thickness/Draw)
сначала мигрирует это пространство и пишет всё одной транзакцией.
- Сохранение marker/layout, HA state, настроек устройства, названия/заливки
пространства и простое открытие карточки не запускают migration write.
- **«Оптимизировать планы»** строит preview и мигрирует все legacy-пространства
одной подтверждённой транзакцией.
- Full/space import старого документа проецирует и валидирует migration в
preview; Apply сохраняет canonical v9 candidate либо отказывает целиком.
### 10.2 Масштаб и round-trip
Координаты переносимых `a/b` проходят существующий lattice write barrier.
Space-only import с другим `cell_cm` сохраняет нормализованную геометрию и
сантиметровый `cm:0` без масштабирования значения. Full export/import и
config/layout round-trip не восстанавливают deprecated fields.
## 11. Backend, import/export и concurrency
- `custom_components/houseplan/validation.py` принимает новый enum и сохраняет
существующий v8 диапазон `wall_segments[].cm = 0..100`.
- Validation v9 запрещает zero opening host, non-finite/negative cm, нарушение
catalog/room projection и canonical наличие `open_spans/open_to`; legacy
document допускается только через compatibility read/import path.
- `import_export.py` включает `zero_wall_style`, умеет preview старой миграции и
выдаёт локализуемый/машиночитаемый blocker code.
- Config revision/CAS и атомарная config+layout запись остаются обязательными;
конфликт ревизии не повторяет migration поверх устаревшего candidate.
- Permissions, HA service calls, entity registry и security boundary не
меняются. Новые данные не исполняются и проходят существующие bounds.
## 12. Совместимость и откат
### 12.1 Forward compatibility
Старые `open_spans/open_to` до записи продолжают читаться. Одновременно runtime
применяет единый zero-wall mode ко всем v8 `cm:0`: при отсутствующей настройке
это `dashed`, поэтому ранее bodyless-физические оси могут стать пунктирными и
начать пропускать свет уже после обновления. Этот переход намеренно не является
lossless по визуалу/свету; координаты, stable IDs, комнаты, положительные
толщины и сами opening records не меняются. Backend сохраняет compatibility-
reader минимум весь релизный цикл v1.68.x; удаление reader требует отдельного
issue и данных telemetry/fixtures.
### 12.2 Downgrade / rollback
Версия до #306 понимает v8 `cm:0`, но не понимает их новую единую семантику и
может снова записать `open_spans/open_to`. Поэтому downgrade после canonical v9
write не поддерживается. До первой структурной записи пользователь может сделать
полный backup. После Optimize доступен существующий one-deep Undo в текущей
сессии; надёжный откат между версиями — импорт backup.
Релизный rollback до первой beta выполняется revert коммита. После beta нельзя
возвращать старую schema в stable; исправление выпускается forward-only с
сохранением v8 reader. Labs-флаг не используется: две одновременно пишущие
модели создадут больший риск, чем feature flag способен снять.
## 13. Производительность
- Migration/atomization выполняется только на structural write/Optimize/import,
не на HA state tick и не на каждом render.
- Runtime projection, wall-role index, light barriers и render geometry
fingerprint/cache включают `space id + geometry revision + zero_wall_style`.
- Смена HA state не пересобирает wall atoms, connectivity или sun barriers.
- Лимит authoritative-каталога остаётся равен backend-константе
`MAX_WALL_SEGMENTS` (сейчас 200 000); результат не truncates.
- Benchmark на large-house fixture сравнивает v8 compatibility projection и v9 canonical:
p95 построения barrier/first render не должен регрессировать более чем на 10%,
steady HA-state render — более чем на 5% относительно baseline ветки.
- Нулевые line barriers не превращаются в полигоны, что ограничивает рост
polyclip input.
## 14. Touch и accessibility
- View/kiosk: нулевые стены, свет и `show_borders` обязаны совпадать с desktop;
pinch/tap не оставляет sticky hover и не меняет стену.
- Plan editor на touch — best effort / intentionally degraded по
`docs/TOUCH-SUPPORT.md`, но cancel/pinch/scroll не имеют права совершить write.
- Select/radio настройки имеет label, keyboard focus и видимое пояснение;
ошибка проёма/неоднозначности доступна через существующий status/toast flow.
- Контраст dashed/solid использует текущие wall tokens; смысл не кодируется
только цветом.
- `prefers-reduced-motion` не требует отдельного поведения: новая фича не
добавляет animation.
## 15. i18n
Добавляются симметрично в `src/i18n/ru.json` и `src/i18n/en.json`:
| Key | RU | EN |
|---|---|---|
| `space.zero_wall_style` | Стены нулевой толщины | Zero-thickness walls |
| `space.zero_wall_dashed` | Пунктирные | Dashed |
| `space.zero_wall_solid` | Сплошные | Solid |
| `space.zero_wall_help` | Пунктирные стены пропускают свет. Сплошные стены блокируют свет, даже при нулевой толщине. | Dashed walls let light through. Solid walls block light even at zero thickness. |
| `toast.zero_wall_opening_conflict` | Сначала удалите проём на этом участке стены. | Remove the opening on this wall segment first. |
| `toast.zero_wall_ambiguous` | Не удалось однозначно выбрать участок стены. Уточните геометрию узла. | The wall segment is ambiguous. Simplify or adjust the junction. |
| `toast.zero_wall_migration_blocked` | Пространство не преобразовано: {reason}. Данные не изменены. | The space was not converted: {reason}. No data was changed. |
| `gs.zero_walls_migrated` | Преобразовано виртуальных участков: {n}. | Virtual wall spans converted: {n}. |
Существующие `boundary.*`, `toast.boundary_*` и подписи инструмента удаляются
только после проверки отсутствия consumers. Compatibility import error codes
локализуются UI, backend возвращает стабильный machine code без текста интерфейса.
## 16. Затронутые модули
Обязательный минимум; точное разбиение большого `houseplan-card.ts` допускается
без изменения контракта:
- `src/zero-walls.ts` — новый canonical resolver/migration;
- `src/types.ts`, `src/space-geometry.ts`, `src/logic.ts` — schema/runtime model;
- `src/wall-thickness.ts`, `src/physical-geometry.ts`, `src/wall-face-graph.ts`,
`src/wall-merge.ts` — explicit zero, role-aware normalization, no body;
- `src/open-spans.ts` — оставить compatibility adapter либо заменить им; новый
production write не импортирует legacy mutators;
- `src/houseplan-card.ts` — убрать Boundary UX/session; Draw/Thickness/settings,
render, Undo, Split/Merge/Delete/Resize, cache invalidation;
- `src/light-visibility.ts`, `src/sun.ts`, `src/iso-walls.ts` — единый light mode
и visual parity;
- `src/plan-optimizer.ts`, `src/plan-geometry-preflight.ts`,
`src/wall-segment-model.ts`, `src/coordinate-canonicalization.ts` — migration
v9 поверх identity barrier #282, limits/idempotence;
- `custom_components/houseplan/const.py`, `validation.py`, `import_export.py`,
`coordinate_canonicalization.py` — model version, schema и import/export;
- `src/i18n/en.json`, `src/i18n/ru.json`;
- `docs/WALL-THICKNESS.md`, `docs/LIGHT.md`, `docs/USER-GUIDE.md`,
`docs/USER-GUIDE.ru.md`,
`docs/CONFIG-COMPATIBILITY.md`, `docs/ARCHITECTURE.md`, `docs/STATUS.md`, оба
changelog;
- unit/backend/smoke/golden/performance fixtures из §17.
Противоречащие актуальные ТЗ #148 и #173 получают короткую superseded-note со
ссылкой на #306; история их acceptance contract не переписывается задним числом.
## 17. Acceptance criteria и доказательства
### AC1. Один пользовательский инструмент
В Plan editor нет кнопки/режима/подсказок «Граница». «Стены» принимают 0 и
создают нулевой room wall, draft или partition тем же pointer/keyboard flow.
**Доказательство:** source-contract unit + `demo/smoke_zero_walls.mjs` desktop;
golden основной панели до/после.
### AC2. Толщина переключается в обе стороны без побочного изменения
Для цепочки `15 → 0 → 20` инструмент «Толщина» переводит только выбранный атом
`0 ↔ positive`; соседи, ownership, coordinates и room topology неизменны.
Пустое поле не записывает ноль.
**Доказательство:** table-driven unit `test/wall-thickness.test.mjs` и smoke с
Undo/Redo.
### AC3. Все `cm:0` имеют одну семантику
Для v8 plan с двумя нулевыми contour atoms — один получен из прежней физической
оси без тела, второй совпадает с `open_spans` — runtime и v9 migration не
различают происхождение. Оба следуют одному `zero_wall_style`; persisted
`zero_kind`/compatibility-marker отсутствует. При default `dashed` оба становятся
пунктирными и пропускают свет.
**Доказательство:** migration/runtime unit на смешанной v8 fixture + source-
contract, запрещающий discriminator происхождения.
### AC4. Физическая геометрия и площадь не получают тело
Shared, outer и independent `cm:0` дают нулевой wall-body contribution,
не меняют чистую площадь и не создают paper/hatch/tunnel. Положительные соседи
сохраняют точные mitre/end geometry.
**Доказательство:** `test/physical-geometry.test.mjs`, area unit и golden flat +
isometric на смешанной цепочке.
### AC5. Пунктир пропускает оба вида света
В `dashed` Glow и солнечный луч проходят shared/outer/independent zero line;
shared atom соединяет room light zones. Положительные стены продолжают блокировать.
**Доказательство:** unit light matrix (`test/light-visibility.test.mjs`,
`test/sun.test.mjs`) + deterministic Glow/sun golden.
### AC6. Сплошная нулевая стена блокирует оба вида света без фиктивного тела
В `solid` та же ось блокирует Glow и солнечные лучи и не соединяет комнаты, но
area/wall body остаются идентичны dashed-варианту.
**Доказательство:** shared fixture matrix и assertions, что physical geometry и
area byte/number equal между стилями; golden solid.
### AC7. Смена настройки работает без reload
Переключение `dashed ↔ solid` сразу меняет line style, Glow, sun и connectivity;
старые barrier/render caches не используются, координаты и `wall_segments[]` не
меняются.
**Доказательство:** unit fingerprint/cache test + browser smoke в одном session.
### AC8. Миграция `open_spans` сохраняет данные и idempotent
Полный/частичный/соседний span, разные positive residues и shared/outer role
преобразуются в exact `cm:0` atoms; stable IDs сохраняются по lineage #282,
deprecated fields удаляются только в candidate, model становится v9. Второй
запуск no-op. Координаты/комнаты/opening records не теряются; визуальная и
световая losslessness для прежних bodyless `cm:0` намеренно не обещается.
**Доказательство:** `test/zero-wall-migration.test.mjs` fixture matrix и snapshot
до/после/после второго запуска.
### AC9. Legacy `open_to` fallback мигрирует только доказанную общую границу
При отсутствии `open_spans` симметричная или односторонняя legacy link создаёт
полный shared zero interval. При наличии spans link их не расширяет. Missing or
ambiguous room id блокирует write без partial result.
**Доказательство:** migration unit на winding/order/id variants.
### AC10. Проёмы защищены от потери
Новый проём нельзя разместить на zero atom. Positive→zero и legacy migration с
пересекающимся opening отклоняются атомарно, сохраняют opening и исходные стены.
**Доказательство:** `test/opening-placement.test.mjs`, backend validation и browser
smoke с RU/EN error lookup.
### AC11. Операции геометрии сохраняют ноль и роль
Split, Merge, Delete (оба варианта), Resize, room creation, повторное рисование,
T/X junction, совпадающая partition, Undo/Redo не clamp'ят zero и не создают
duplicate carrier/mixed-role record.
**Доказательство:** pure matrix в wall/resize/room-deletion tests +
`demo/smoke_edit_walk.mjs` deterministic seeds; ambiguous overlap asserts no write.
### AC12. Read-only загрузка ничего не мигрирует
Открытие View, HA state update, marker/layout save и изменение неструктурной
настройки не вызывают config write/model bump. Первая geometry write конкретного
space мигрирует его в той же CAS transaction.
**Доказательство:** mutation-gate unit + browser websocket call log.
### AC13. Optimize и import/export атомарны
Optimize preview показывает counts и мигрирует все spaces после Confirm; one-deep
Undo возвращает исходные v8/legacy data в сессии. Full/space v8 или более старый
import создаёт v9 candidate через последовательные identity и zero-wall barriers.
Превышение любого актуального backend-лимита, invalid span, opening conflict
или revision conflict отклоняет весь candidate без truncation/partial apply.
**Доказательство:** optimizer unit, `tests_backend/test_ha_import_export.py`,
`tests_backend/test_validation.py`, browser backup transfer smoke.
### AC14. View/isometric/editor/export parity
Один и тот же style виден в flat, isometric, Plan/Decor contexts и visual export.
`show_borders:false` скрывает линию в View/kiosk, но не меняет выбранную световую
семантику; Plan axes/nodes видны во всех режимах редактора.
**Доказательство:** golden matrix desktop + narrow viewport; source assertions и
smoke `show_borders`.
### AC15. Backend и compatibility registry согласованы
Backend принимает canonical v9 `wall_segments[].cm:0` с exact endpoints,
отклоняет negative, non-finite, zero opening host и legacy virtual fields в v9.
Старый v8/pre-v8 документ читается через compatibility path.
`docs/CONFIG-COMPATIBILITY.md` описывает оба deprecated поля и downgrade.
**Доказательство:** backend parameterized tests + docs review.
### AC16. Touch safety floor
На touch View/kiosk линия и свет совпадают с desktop. Pinch/cancel в Plan editor
не записывает нулевой сегмент и не оставляет активную command session.
**Доказательство:** Playwright touch smoke; ручное редактирование сверх этого —
best effort и не блокирует.
### AC17. Производительность и кэши
Large-house v8 projection/v9 canonical проходят бюджеты §13; HA state tick не
запускает atomization/barrier rebuild; style toggle запускает ровно одну
инвалидацию требуемых geometry/light caches.
**Доказательство:** benchmark artefact JSON + source-fingerprint unit.
### AC18. Документация и релизные артефакты
RU/EN UI, changelog и user docs описывают одну систему стен, новый диапазон,
стиль/свет и backup перед migration. ТЗ #148/#173 помечены superseded в части
Boundary. Bundle-копии синхронны.
**Доказательство:** i18n/docs/bundle tests и ревью артефактов.
## 18. Обязательные проверки реализации
Минимальный implementation gate до code review:
```text
npm run typecheck
npm test
npm run build
uv run pytest tests_backend/test_validation.py tests_backend/test_ha_import_export.py
```
Code-review/release gate дополнительно выполняет:
- `demo/smoke_zero_walls.mjs` desktop + touch safety;
- Glow и sun deterministic golden matrix;
- flat/isometric/show_borders golden review;
- large-house performance capture с бюджетами §13;
- полный импорт v8 и pre-v8 full/space fixtures и v9 round-trip;
- сверку `dist/houseplan-card.js` и
`custom_components/houseplan/frontend/houseplan-card.js`.
Golden принимаются только через действующую policy после визуального review;
изменение baseline ради зелёного CI запрещено.
## 19. Release-артефакты
В том же коммите, что пользовательское поведение:
- `docs/CHANGELOG.md` и `docs/CHANGELOG.ru.md`: удаление «Границы», стены `0`,
настройка пунктир/сплошная и предупреждение о migration backup;
- `docs/USER-GUIDE.md`, `docs/USER-GUIDE.ru.md`, `docs/WALL-THICKNESS.md`,
`docs/LIGHT.md`;
- `docs/CONFIG-COMPATIBILITY.md`, `docs/ARCHITECTURE.md`, `docs/STATUS.md`;
- RU/EN screenshots/golden: toolbar, space setting, dashed/solid flat и iso,
Glow/sun matrix;
- performance JSON и полный review-документ;
- security artefact не требуется: permissions/service boundary не меняется;
backend bounds покрываются tests.
## 20. Риски и защита
| Риск | Защита |
|---|---|
| Отсутствующая thickness-запись ошибочно станет open | explicit-presence invariant + AC3 |
| Потеря positive residue при partial span | breakpoint atomization + snapshot AC8 |
| Проём исчезнет/переедет | migrate blocker и запрет positive→zero |
| Glow и sun разойдутся | единый resolver, совместная матрица AC5/AC6 |
| `0` превратится в `1` старым clamp | отдельный zero-aware parser + AC2/AC11 |
| Atomization превысит лимит | atomic failure, no truncation |
| Старый клиент вернёт legacy-поля в v9 | документированный unsupported downgrade + backup |
| Cache переживёт style toggle | style in fingerprint + AC7/AC17 |
| Две модели продолжат писаться | source-contract запрещает production legacy writers |
## 21. Зависимости и вне скоупа
### Зависимости
- #33 — compatibility lifecycle;
- #173 — единый инструмент «Стены»;
- #199 — geometry preflight;
- #224/#299 — canonical coordinates и role-aware records;
- #282 — реализованная обязательная основа: stable `wall_segments[].id`,
`rooms[].wall_ids` и единый structural identity barrier. #306 не дублирует и
не обходит этот writer.
### Вне скоупа
- Stages 2–4 ADR #282 (integer lattice, единый planar graph, closed-form junctions);
- разный style/light mode у отдельных zero walls;
- проёмы в нулевых стенах и «неактивные» сохранённые проёмы;
- создание внешней световой зоны;
- автоматическое разделение HA area независимой стеной;
- полноценная гарантия Plan editor на touch сверх safety floor;
- удаление compatibility reader в том же релизе.