Files
houseplan-card/docs/specs/157-open-passage.md
T
2026-08-17 16:25:15 +03:00

43 KiB
Raw Blame History

#157 — тип проёма «Открытый проём»

  • Issue: #157
  • Приоритет: P2
  • Ветка: issue/157-open-passage
  • Статус документа: замечания первого раунда review учтены; issue остаётся в S3-spec по прямому указанию владельца
  • Основание: issue владельца от 2026-08-14, аналитика от 2026-08-15 и review от 2026-08-17

1. Пользовательская проблема и результат

На реальном плане часто есть арка или обычный открытый дверной проём: стена сохраняется по сторонам, но полотна двери и связанных с ним устройств нет. Сейчас такой объект приходится изображать дверью либо виртуально открытым участком стены. Дверь добавляет ложную створку и предлагает датчик/замок, а виртуальный участок означает отсутствие кладки и отмечается пунктиром в редакторе.

После #157 пользователь получает четвёртый тип обычного проёма — passage / «Открытый проём». Он:

  • вырезает реальный участок кладки;
  • не рисует створку, дугу, пунктир или самостоятельную рамку;
  • продолжает пол через существующую геометрию opening tunnel;
  • всегда пропускает свет между двумя распознанными комнатами;
  • не имеет датчика, замка и параметров створки;
  • доступен в существующем потоке размещения и редактирования проёмов.

2. Подтверждённая техническая база

На момент подготовки ТЗ:

  • OpeningCfg.type и backend schema знают только door, window, gate;
  • Full card уже вырезает из wall body все сохранённые opening geometry и рисует room-coloured tunnel fill;
  • renderOpeningVisibleGeometry() имеет fallback-ветку двери, поэтому простого расширения union недостаточно: passage ошибочно получил бы дверную створку;
  • Plan сохраняет невидимый hitbox и hover outline отдельно от видимого символа; этот механизм можно переиспользовать для выбора passage;
  • свет уже считает door/gate внутренним проходом только при наличии пола с обеих сторон и оставляет внешний проём непрозрачным;
  • скрытая изометрия вырезает opening geometry из стены отдельно от построения полотен, но fallback в buildIsoOpeningBasis() также превращает неизвестный тип в дверь;
  • Static card сейчас не рисует opening symbols и не подаёт opening cuts в свою wall geometry;
  • CONFIG_SCHEMA сохраняет неизвестные sibling-поля opening благодаря extra=vol.ALLOW_EXTRA, а marker validators уже задают принятый образец change-aware проверки «старое битое можно прочитать и пронести без изменения, новое битое записать нельзя»;
  • full-card и static-card snapshots сейчас подписываются на contact/lock каждого проёма без проверки типа.

3. Нормативные продуктовые решения

  1. Сохраняемый literal нового типа — только passage.
  2. Пользовательское название:
    • RU: Открытый проём;
    • EN: Open passage.
  3. Новый проём размещается тем же инструментом, на тех же физических стенах и с теми же правилами snap/центрирования/измерений, что дверь.
  4. Default ширины нового проёма — 90 см, как у двери.
  5. В покое passage не имеет собственного архитектурного символа. Видим только физический разрыв кладки и пол внутри него.
  6. В Plan допускается временный editor chrome: hover/selection outline, hitbox, центральная точка preview и размерные подсказки. Это не часть итогового условного обозначения и не показывается в View/Static.
  7. contact, lock, invert, flip_h, flip_v для passage неприменимы.
  8. При переходе существующего проёма в passage сохранение удаляет эти пять известных полей. Если были непустые contact или lock, до сохранения показывается явное предупреждение.
  9. Обратный переход из сохранённого passage не восстанавливает удалённые привязки или флаги.
  10. Для света passage прозрачен только когда проба по обе стороны стены попадает на пол комнаты. Внешний либо неопределённый край остаётся fail-dark. Любой неизвестный будущий тип проёма также считается непрозрачным: переход от прежнего неявного type !== 'window' к явному allowlist — осознанное ужесточение forward-совместимости ради защиты от утечки света.
  11. passage отличается от open span наличием кладки по сторонам и физических откосов. Open span по-прежнему означает отсутствие кладки на всём участке и показывается пунктиром только в Plan.
  12. Публичность скрытого изометрического режима не меняется.

4. Scope

4.1 Модель и backend

  • расширить TypeScript union и backend enum литералом passage;
  • добавить change-aware semantic validator неприменимых полей;
  • подключить его ко всем путям записи и импорта;
  • сохранить lossless round-trip незатронутых legacy/future данных.

4.2 Plan editor

  • добавить passage в submenu инструмента проёмов;
  • добавить четвёртый radio-вариант в диалог;
  • применить default 90 см;
  • скрывать binding/flip/invert controls;
  • показывать предупреждение о снятии существующих привязок;
  • корректно создавать, выбирать, перетаскивать, менять тип и удалять объект;
  • сохранять обычный geometry history/undo contract.

4.3 Рендер и свет

  • Full 2D Plan/View;
  • Static card;
  • скрытая изометрия;
  • общий wall cut и opening tunnel fill;
  • Glow/light barrier geometry;
  • исключение stale binding-полей passage из runtime подписок и UI.

4.4 Совместимость и документация

  • full/space export-import;
  • downgrade note;
  • RU/EN i18n;
  • пользовательское руководство, LIGHT, ISOMETRIC и CONFIG-COMPATIBILITY;
  • unit/backend tests, smoke и golden evidence.

5. Non-scope

  • моделирование криволинейной арки, верхней перемычки или высоты проёма;
  • выбор формы проёма, материала откосов или декоративной обналички;
  • датчик присутствия либо иной entity binding для passage;
  • изменение прозрачности по состоянию сущности;
  • публичное включение изометрии;
  • общая parity-задача по отображению door/window/gate в Static card;
  • автоматическое преобразование существующих дверей или open spans;
  • изменение формата open spans;
  • изменение существующей семантики дверей, окон и ворот.

В этой задаче «арка» — пользовательское название открытого проёма. Геометрически это прямоугольный полноразмерный разрыв стены в текущей 2.5D-модели.

6. Контракт данных

6.1 TypeScript

export interface OpeningCfg {
  id: string;
  type: 'door' | 'window' | 'gate' | 'passage';
  x: number;
  y: number;
  angle: number;
  length: number;
  contact?: string | null;
  lock?: string | null;
  invert?: boolean;
  flip_h?: boolean;
  flip_v?: boolean;
}

Общие optional-поля остаются в интерфейсе ради совместимости чтения. Для канонической записи type: 'passage' допустимы только id, type, x, y, angle, length и неизвестные текущей версии extension-поля.

Пример канонической записи:

{
  "id": "o-passage-hall",
  "type": "passage",
  "x": 0.417,
  "y": 0.286,
  "angle": 90,
  "length": 0.09
}

6.2 Defaults и ограничения

  • lengthCm = 90 при новом размещении;
  • существующие ограничения диалога 20..600 см, step 5 см сохраняются;
  • x, y, angle, length, id generation и нормализация не меняются;
  • версия модели и migration step не добавляются;
  • существующие три литерала и их сериализация не меняются.

6.3 Канонизация из UI

При сохранении passage frontend:

  1. записывает актуальные id/type/geometry;
  2. удаляет contact, lock, invert, flip_h, flip_v, а не записывает null/false;
  3. при редактировании существующего opening сохраняет неизвестные sibling-поля;
  4. не меняет другие openings и их порядок.

Явное редактирование passage может канонизировать перечисленные известные неприменимые поля. Несвязанное сохранение не должно переписывать объект.

7. Backend validation и broken-read contract

7.1 Schema

Inline opening schema в validation.py принимает четвёртый enum literal. Поля contact, lock, invert, flip_h, flip_v остаются syntactically readable, чтобы загрузка и round-trip старых/будущих данных не ломались до semantic stage.

7.2 Semantic validator

Добавляется чистая функция, например:

validate_opening_passages(
    config: dict,
    previous: dict | None = None,
    *,
    validate_all: bool = False,
) -> None

Она сопоставляет пространства по space.id, openings внутри пространства по opening.id и применяет правила:

Ситуация Результат
Новый passage без запрещённых ключей принять
Смена другого типа на passage, запрещённый ключ остался отклонить
В существующем passage добавлен или изменён любой запрещённый ключ отклонить
Неизменённый некорректный passage проходит с несвязанной правкой принять без переписывания
Из некорректного passage удалены запрещённые ключи принять
Full/space import содержит любой запрещённый ключ у passage отклонить

Запрещённым считается само наличие ключа, включая contact: null и flip_h: false: каноническая новая запись не содержит неприменимых полей.

Если старый объект имел другой type, смена контекста на passage считается новой семантикой и проверяется полностью, даже когда значение binding-поля текстово не изменилось.

Стабильный public error code: invalid_passage_fields. Технический payload содержит id пространства, id проёма и отсортированный список запрещённых полей, но не значения entity ids. Frontend локализует ошибку человекочитаемо: У открытого проёма в комнате «<название>» есть недопустимые параметры: … / The open passage in room “<name>” has unsupported settings: …. Если название комнаты отсутствует, используется id пространства; id проёма остаётся в технических деталях, а не является единственным ориентиром в пользовательском сообщении.

7.3 Точки подключения

Одна и та же проверка обязательна для:

  • houseplan/config/set с предыдущей конфигурацией;
  • houseplan/plan/optimize с предыдущей конфигурацией;
  • merge-import с предыдущей конфигурацией;
  • replace/full import с validate_all=True;
  • space import с validate_all=True для входящего/перенумерованного content.

Ошибка преобразуется в существующий websocket/import error contract; partial write запрещён.

8. UX редактора

8.1 Палитра и размещение

В submenu «Проём» добавляется пункт:

  • label: Открытый проём / Open passage;
  • icon: mdi:arch;
  • id/type: passage;
  • default: 90 см, flipH=false, flipV=false.

Порядок пунктов: окно, дверь, открытый проём, ворота. Hover/click resolver, physical-wall eligibility, запрет на open span, center magnet, shoulder rulers и commit flow полностью общие с остальными openings.

Preview нового типа не рисует дверной symbol. Он показывает только существующие временные элементы размещения: центральную точку, размерные подписи и нейтральный selection footprint. Пунктир open span и створка не используются.

8.2 Диалог

Диалог содержит четвёртый radio option. При type === 'passage':

  • поле ширины остаётся;
  • contact selector скрыт;
  • invert скрыт;
  • lock selector скрыт;
  • flip horizontal и flip vertical скрыты;
  • icon диалога — mdi:arch;
  • Save и Delete работают как для остальных openings.

Если draft содержит непустой contact или lock, после выбора passage показывается видимый inline warning:

  • RU: При сохранении датчик открытия и замок будут удалены.;
  • EN: Saving will remove the open/close sensor and lock.

Текст может грамматически перечислять только реально заполненные поля, но смысл и наличие предупреждения обязательны. Warning имеет role="status" и не полагается только на цвет или icon.

Переключение radio само по себе не изменяет config. Пока диалог не сохранён, скрытые значения остаются в draft: если пользователь вернулся к исходному типу, поля снова видны. Save с passage удаляет их. Отдельный confirm modal не нужен: явное предупреждение в открытом commit-диалоге удовлетворяет продуктовой формулировке и не добавляет второй уровень подтверждения.

Если прочитан уже сохранённый некорректный passage со stale bindings, диалог показывает то же предупреждение, а Save канонизирует запись. Cancel ничего не меняет.

8.3 История и взаимодействие

  • create/edit/type change/delete используют существующие history labels;
  • drag меняет только geometry и остаётся одним history step;
  • undo/redo восстанавливает целиком прежний opening, включая type и bindings;
  • в Plan невидимый opening выбирается существующим hitbox и показывает hover outline;
  • в View и Static passage inert и не открывает info card;
  • stale contact/lock у прочитанного passage не создают lock badge, opening info или live entity subscription.

9. Нормативная матрица рендера

Surface/state Кладка Пол/tunnel Собственный symbol Editor chrome
Full View разрыв по length есть нет нет
Full Plan, покой разрыв по length есть нет только при hover/selection
Full Plan, placement будущая позиция читается по preview/размерам preview не коммитит geometry нет створки/пунктира да
Static card разрыв по length есть нет нет
Скрытая изометрия полноразмерный вертикальный разрыв, видимые откосы существующий floor нет полотна и его тени нет

9.1 Full 2D

  • renderOpeningVisibleGeometry() обязан иметь явную ветку passage, возвращающую пустую visible geometry; fallback неизвестного типа не должен превращать его в дверь.
  • openingVisibleMetrics() продолжает выдавать размеры hitbox/outline по длине и толщине стены.
  • wall cut и opening tunnel используют существующий общий pipeline без отдельной декоративной рамки.
  • openingAmount('passage', ...) всегда возвращает 1 и не читает contact.
  • lock badge разрешён только для door и gate, а не по условию type !== 'window'.
  • hide_openings скрывает symbols старых типов, но не заделывает физический разрыв passage.

9.2 Static card

Static должен показать новый тип, но #157 не меняет исторический визуал трёх старых типов. Поэтому static wall fingerprint и wall builder получают только нормализованные cuts с type === 'passage'; door/window/gate по-прежнему не добавляют Static symbols/cuts в рамках этой задачи.

Для плана без passage output Static и cache fingerprint остаются прежними. При наличии passage canonical wall geometry и tunnel/floor layer должны дать чистый разрыв и пол, в том числе при default wall thickness. Никаких HTML hit-targets или entity subscriptions для passage Static не создаёт.

9.3 Изометрия

  • IsoOpeningType расширяется passage;
  • structural basis для passage содержит пустой список leaves;
  • opening остаётся во входных cuts wall boolean geometry;
  • projection и bounds безопасно обрабатывают пустой basis;
  • не рисуются panel, leaf shadow и live-state animation;
  • откосы являются частью общей extrusion geometry стены;
  • высота/форма арки не моделируются: cut идёт на полную текущую высоту стены.

10. Свет и Glow

Классификация проходов должна стать явной, а не зависеть от условия type !== 'window':

  • door, gate, passage — кандидаты interior passage;
  • window — не passage;
  • любой неизвестный тип — opaque/fail-dark и не попадает в cuts, пока новый literal не будет явно классифицирован и покрыт тестом;
  • каждый кандидат проходит одинаковую проверку пола по обе стороны;
  • только прошедшие кандидаты вычитаются из light masonry и добавляются в cuts;
  • passage не зависит от HA state, contact и invert, даже если эти поля присутствуют в старой битой записи;
  • наружный passage, passage у пустой стороны или на нераспознанной геометрии остаётся непрозрачным для light;
  • sun/window rays по-прежнему используют только windows;
  • ширина прозрачного участка и Glow footprint совпадают с открытой дверью той же geometry при одинаковом floor test.

Fingerprint/cache key света должен зависеть от geometry passage так же, как от других interior passages. Изменение type door↔passage при неизменной geometry не обязано перестраивать barriers, если итоговая классификация та же; correctness важнее этой оптимизации.

11. Import, export и совместимость

11.1 Текущая версия

  • full export и space export сохраняют literal passage без remap;
  • import remap меняет id пространства/opening по существующим правилам, но не type и geometry;
  • valid passage проходит full/space import;
  • passage с запрещённым ключом отклоняется до commit;
  • неизвестные sibling-поля сохраняются backend schema и при несвязанном round-trip;
  • оптимизация не удаляет и не превращает passage.

11.2 Старые конфигурации

Конфигурация без passage не мигрируется, не переписывается и не меняет визуальное или runtime-поведение. Нового top-level capability flag не требуется.

11.3 Downgrade

Перед реализацией текущий pre-feature frontend фактически попадает для неизвестного opening type в дверную fallback-ветку. Поэтому ожидаемый best-effort downgrade для frontend v1.64.0:

  • загрузка и рендер не падают;
  • passage может выглядеть как дверь, то есть визуальная точность не гарантируется;
  • старый диалог не знает radio option и не должен считаться поддерживаемым редактором этого типа;
  • старый backend со строгим enum может отклонить последующую запись всей конфигурации, содержащей passage.

Эти ограничения явно записываются в docs/CONFIG-COMPATIBILITY.md. Реализация обязана исполнением загрузить fixture с passage в собранный из git tag v1.64.0 bundle через demo harness и подтвердить отсутствие необработанного исключения в консоли. Source-аудит не считается достаточным доказательством; поддержка редактирования на старой версии не требуется.

12. I18n, accessibility и touch

Минимальный набор новых RU/EN ключей:

  • opening.passage;
  • opening.passage_binding_warning;
  • при необходимости отдельный help/description key для отличия от open span.

Требования:

  • RU/EN key parity test проходит;
  • radio имеет обычную label association и доступен с клавиатуры;
  • warning читается screen reader и не кодируется одним цветом;
  • toolbar item получает локализованный accessible label через существующий menu contract;
  • новый тип не уменьшает существующие touch targets;
  • невидимый passage hitbox в Plan сохраняет существующий минимум; во View его pointer events отключены по общему contract openings.

13. Performance, security и observability

  • отдельного runtime animation/state resolver для passage нет;
  • stale bindings не попадают в full/static snapshot entity sets;
  • wall, light и iso caches используют существующую geometry/fingerprint архитектуру; новый unbounded cache не добавляется;
  • change-aware validator работает линейно по spaces/openings с map по id;
  • error не логирует entity ids и не раскрывает больше существующей config API;
  • права websocket/import не меняются;
  • отдельного benchmark profile не требуется; перед бетой проходит канонический performance gate всего продукта.

14. Acceptance criteria

  1. В toolbar и opening dialog доступен Открытый проём / Open passage с default 90 см.
  2. Для passage показывается только ширина; contact, lock, invert, flip_h, flip_v скрыты и не записываются.
  3. При смене объекта с непустым contact/lock на passage до Save виден явный warning; Cancel сохраняет исходник, Save удаляет bindings, обратная смена после Save их не восстанавливает.
  4. Full View/Plan показывает чистый физический разрыв стены и продолжение пола, без створки, дуги, пунктира или рамки.
  5. Passage остаётся выбираемым, перемещаемым и удаляемым в Plan через временный editor chrome, но inert в View/Static.
  6. Static card показывает passage как разрыв с полом; план без passage и старые opening types сохраняют прежний Static output.
  7. Скрытая изометрия показывает полноразмерный cut и откосы, без leaf/panel shadow и без crash.
  8. Внутренний passage пропускает light/Glow независимо от state; наружный, неопределённый passage и неизвестный будущий тип остаются fail-dark.
  9. Backend принимает канонический passage и отклоняет новую/изменённую запись с любым из пяти запрещённых ключей кодом invalid_passage_fields.
  10. Неизменённый legacy-broken passage проходит несвязанное сохранение; его исправление принимается; import валидирует весь входящий content.
  11. Full/space export-import сохраняют type/geometry и обычный id remap; partial write при ошибке отсутствует.
  12. Конфиги с door/window/gate не мигрируют и проходят regression suite без изменений.
  13. Demo harness с реально собранным bundle v1.64.0 загружает fixture с passage без необработанного исключения в консоли; точный fallback и ограничение старого backend задокументированы.
  14. В scripts/mutation-gate.mjs зарегистрированы и проходят пять обязательных executable mutants из §15.6; --check подтверждает уникальность их якорей.
  15. RU/EN parity, unit/backend tests, build, smoke и новые golden evidence проходят в предусмотренный процессом момент.

15. Тест-план

15.1 TypeScript unit

  • OpeningPlacementType/preset: passage, 90 см, false flips;
  • placement на physical wall, запрет на open span, center magnet и measurements не расходятся с door;
  • openingAmount('passage', null/on/off/unavailable, invert) всегда 1;
  • renderOpeningVisibleGeometry(passage) не содержит leaf/arc/glass/gate path;
  • metrics/hitbox остаются конечными и положительными;
  • full renderer не создаёт lock badge/info/binding subscription для stale passage fields;
  • light: две комнаты пропускают, внешний край не пропускает, footprint равен door при одинаковой geometry; искусственный тип xyz не проходит explicit allowlist и остаётся opaque;
  • Static fingerprint/output меняется для passage и не меняется для старых типов;
  • iso basis passage имеет zero leaves, projection/bounds безопасны;
  • RU/EN parity.

15.2 Backend unit

  • schema принимает canonical passage и прежние три types;
  • новый/сменивший тип passage с каждым запрещённым ключом, включая null/false, отклоняется;
  • одновременный список полей даёт стабильный отсортированный error;
  • неизменённый broken passage + unrelated config edit принимается;
  • изменение geometry при неизменных broken fields принимается как несвязанное с этими полями изменение;
  • удаление broken fields принимается;
  • изменение/добавление broken field отклоняется;
  • config/set и optimize вызывают validator;
  • merge, full replace и space import покрыты success/failure/atomicity cases;
  • valid export-import round-trip сохраняет type и geometry.

15.3 Smoke/manual

В реализации, а перед бетой — каноническим smoke gate:

  1. создать passage через toolbar на общей толстой стене;
  2. убедиться в preview без створки и в default 90 см;
  3. сохранить, hover/select, перетащить, undo/redo, удалить;
  4. создать дверь с contact+lock, выбрать passage, увидеть warning, Cancel;
  5. повторить и Save; убедиться, что bindings удалены и не восстановились;
  6. проверить Full View и Static в light/dark themes;
  7. включить Glow: внутренний проход светится насквозь, наружный — нет;
  8. проверить скрытую изометрию;
  9. full и space export/import;
  10. checkout tag v1.64.0, собрать его bundle, загрузить fixture с passage через demo harness и автоматически подтвердить отсутствие pageerror и необработанных console errors; приложить команду и лог к evidence.

15.4 Golden

Добавить детерминированную сцену с двумя комнатами, толстой общей стеной, внутренним passage и источником Glow:

  • Full 2D, dark theme;
  • Static, dark theme;
  • скрытая изометрия, dark theme;
  • при необходимости отдельный exterior fail-dark кадр.

Новые baselines предпочтительнее переписывания несвязанных эталонов. Любое изменение существующего smoke_opening_* baseline требует объяснения в PR/code review evidence.

15.5 Команды и момент запуска

В цикле реализации:

npm run typecheck
npm test
npm run build
python -m pytest tests_backend

Golden, browser smoke и performance выполняются перед бетой согласно release runbook. Полный HA harness канонически выполняется в Linux CI; невозможность локального Windows fcntl не считается заменой CI.

15.6 Исполняемый мутационный гейт

Реализация добавляет в scripts/mutation-gate.mjs пять реальных entries. Каждый entry содержит стабильный id, патч find/replace на 2–5 строк с ровно одним якорем, указанный guard и причину ожидаемого падения. Это не список для ручной проверки: runner обязан применить патч в отдельном worktree, собрать bundle, получить non-zero от guard и восстановить чистое состояние.

Mutant id Обязательный патч Guard
passage-visible-geometry-door-fallback удалить/обойти явную passage-ветку в renderOpeningVisibleGeometry(), чтобы снова сработал дверной fallback node --test test/opening-symbol.test.mjs — отрицательный passage-case обязан обнаружить leaf/arc/gate path
passage-light-classifier-removed исключить passage из явного allowlist interior passages node --test test/light-visibility.test.mjs — внутренний passage перестаёт пропускать свет
passage-import-validator-bypassed отключить вызов validate_opening_passages на import-пути node scripts/backend-test-guard.mjs invalid_passage_import — forged full/space import с запрещённым полем обязан быть отклонён
passage-static-door-cut-leak расширить passage-only Static cuts/fingerprint до door, вернув изменение старого плана node --test test/space-geometry.test.mjs — fixed план без passage обязан сохранить прежние geometry и fingerprint
passage-iso-door-fallback вернуть passage в дверную fallback-ветку buildIsoOpeningBasis() node --test test/iso-openings.test.mjs — passage basis обязан иметь zero leaves

Обязательные команды перед передачей реализации в code review:

node scripts/mutation-gate.mjs --check
node scripts/mutation-gate.mjs --id=passage-visible-geometry-door-fallback
node scripts/mutation-gate.mjs --id=passage-light-classifier-removed
node scripts/mutation-gate.mjs --id=passage-import-validator-bypassed
node scripts/mutation-gate.mjs --id=passage-static-door-cut-leak
node scripts/mutation-gate.mjs --id=passage-iso-door-fallback

Каждый чистый guard сначала должен быть зелёным, а лог мутанта — содержать его id, guard и ожидаемый non-zero. Ручное редактирование без runner или только --list/--check acceptance criterion не выполняет.

16. План реализации

  1. Расширить types/schema и добавить чистый semantic validator со всеми call sites и backend tests.
  2. Расширить placement preset, toolbar, dialog draft/save canonicalization, warning и i18n.
  3. Добавить явные passage branches в 2D symbol/amount/locks/subscriptions.
  4. Подключить passage cut/tunnel к Static без изменения output старых типов.
  5. Сделать light classifier явным и покрыть interior/exterior cases.
  6. Добавить zero-leaf passage basis в скрытой изометрии.
  7. Покрыть import/export и исполняемый downgrade fixture на bundle v1.64.0.
  8. Зарегистрировать и исполнить пять mutation entries из §15.6.
  9. Обновить документацию и release artifacts, пройти implementation gates.

17. Release-артефакты

При реализации #157 в том же user-visible commit обязательны:

  • docs/CHANGELOG.md;
  • docs/CHANGELOG.ru.md;
  • docs/USER-GUIDE.ru.md — таблица passage/open span, поток редактора и явное пояснение принятой асимметрии Static: passage разрывает стену, а старые door/window/gate в рамках #157 сохраняют прежний Static-визуал;
  • docs/LIGHT.md — explicit interior-passage rule;
  • docs/ISOMETRIC.md — zero-leaf/full-height-cut поведение скрытого режима;
  • docs/CONFIG-COMPATIBILITY.md — enum, broken-read validator и downgrade;
  • при необходимости docs/TESTING.md — новая smoke/golden сцена;
  • новые reviewed golden baselines и ссылка на evidence в issue/PR.

Коммит продуктового кода должен иметь терминальные trailers:

Issue: #157
User-Visible: yes

18. Риски и меры

Риск Мера
Fallback рисует passage как дверь явные branches и отрицательные renderer tests
Невидимый объект нельзя выбрать сохранить отдельные metrics/hitbox/hover tests
Stale binding продолжает влиять на UI/runtime type allowlist для locks/info/subscriptions, tests с broken fixture
Строгая схема блокирует несвязанные сохранения change-aware semantic validator вместо cross-field schema reject
Import обходит правило подключить validator к обоим import flows и optimize
Static меняет старые планы fingerprint/cuts только для passage, regression snapshots без passage
Свет утекает наружу сохранить двусторонний floor probe и exterior test
Новый неизвестный opening type неявно пропускает свет explicit allowlist, fail-dark contract и unit с типом xyz
Iso создаёт дверное полотно zero-leaf basis и golden
Старый frontend портит новый тип документировать downgrade как read-only best effort

19. Откат

Код можно откатить обычным revert, но уже сохранённый literal passage станет неизвестен старому backend. Поэтому безопасный rollback релиза требует либо предварительно вернуть такие openings к door/gate, либо сохранить frontend и backend read support для passage, отключив только создание в UI. Автоматически превращать passage в дверь при rollback запрещено: это создаёт ложный symbol и может предложить небезопасные lock bindings.

20. Принятые технические предположения

Следующие мелкие решения приняты без дополнительного продуктового вопроса и могут быть изменены при реализации, если не нарушаются acceptance criteria:

  • mdi:arch не поставляется самим card и разрешается runtime-набором MDI в HA; до реализации его наличие проверяется в поддерживаемой минимальной версии HA через demo/browser fixture. Если glyph отсутствует, нормативный fallback — широко поддерживаемый mdi:door-open, без изменения persisted contract;
  • порядок passage в submenu нормативно задан единожды в §8.1;
  • inline warning достаточен, отдельный confirm modal не нужен;
  • draft временно хранит скрытые bindings, чтобы смена radio до Save была обратимой;
  • unknown sibling-поля сохраняются при явном edit, запрещённые известные поля удаляются;
  • наличие запрещённого ключа, даже с null/false, неканонично для новой записи;
  • Static получает только passage cuts, чтобы не расширять #157 до общей opening parity;
  • «арка» в этой версии — полноразмерный прямоугольный cut без кривой перемычки;
  • отдельный performance benchmark не нужен, если общий pre-beta gate не показывает регрессию.