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

49 KiB
Raw Blame History

#306 — Нулевые стены вместо виртуальных границ

  • Issue: #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 Новое поле пространства

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:

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 в том же релизе.