# #157 — тип проёма «Открытый проём» - Issue: [#157](https://github.com/Matysh/houseplan-card/issues/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 ```ts 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-поля. Пример канонической записи: ```json { "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 Добавляется чистая функция, например: ```py 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 “” 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 Команды и момент запуска В цикле реализации: ```text 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: ```text 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: ```text 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 не показывает регрессию.