43 KiB
#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. Нормативные продуктовые решения
- Сохраняемый literal нового типа — только
passage. - Пользовательское название:
- RU:
Открытый проём; - EN:
Open passage.
- RU:
- Новый проём размещается тем же инструментом, на тех же физических стенах и с теми же правилами snap/центрирования/измерений, что дверь.
- Default ширины нового проёма — 90 см, как у двери.
- В покое
passageне имеет собственного архитектурного символа. Видим только физический разрыв кладки и пол внутри него. - В Plan допускается временный editor chrome: hover/selection outline, hitbox, центральная точка preview и размерные подсказки. Это не часть итогового условного обозначения и не показывается в View/Static.
contact,lock,invert,flip_h,flip_vдляpassageнеприменимы.- При переходе существующего проёма в
passageсохранение удаляет эти пять известных полей. Если были непустыеcontactилиlock, до сохранения показывается явное предупреждение. - Обратный переход из сохранённого
passageне восстанавливает удалённые привязки или флаги. - Для света
passageпрозрачен только когда проба по обе стороны стены попадает на пол комнаты. Внешний либо неопределённый край остаётся fail-dark. Любой неизвестный будущий тип проёма также считается непрозрачным: переход от прежнего неявногоtype !== 'window'к явному allowlist — осознанное ужесточение forward-совместимости ради защиты от утечки света. passageотличается от open span наличием кладки по сторонам и физических откосов. Open span по-прежнему означает отсутствие кладки на всём участке и показывается пунктиром только в Plan.- Публичность скрытого изометрического режима не меняется.
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:
- записывает актуальные id/type/geometry;
- удаляет
contact,lock,invert,flip_h,flip_v, а не записываетnull/false; - при редактировании существующего opening сохраняет неизвестные sibling-поля;
- не меняет другие 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
passageinert и не открывает 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
- В toolbar и opening dialog доступен
Открытый проём/Open passageс default 90 см. - Для passage показывается только ширина; contact, lock, invert, flip_h, flip_v скрыты и не записываются.
- При смене объекта с непустым contact/lock на passage до Save виден явный warning; Cancel сохраняет исходник, Save удаляет bindings, обратная смена после Save их не восстанавливает.
- Full View/Plan показывает чистый физический разрыв стены и продолжение пола, без створки, дуги, пунктира или рамки.
- Passage остаётся выбираемым, перемещаемым и удаляемым в Plan через временный editor chrome, но inert в View/Static.
- Static card показывает passage как разрыв с полом; план без passage и старые opening types сохраняют прежний Static output.
- Скрытая изометрия показывает полноразмерный cut и откосы, без leaf/panel shadow и без crash.
- Внутренний passage пропускает light/Glow независимо от state; наружный, неопределённый passage и неизвестный будущий тип остаются fail-dark.
- Backend принимает канонический passage и отклоняет новую/изменённую запись с
любым из пяти запрещённых ключей кодом
invalid_passage_fields. - Неизменённый legacy-broken passage проходит несвязанное сохранение; его исправление принимается; import валидирует весь входящий content.
- Full/space export-import сохраняют type/geometry и обычный id remap; partial write при ошибке отсутствует.
- Конфиги с door/window/gate не мигрируют и проходят regression suite без изменений.
- Demo harness с реально собранным bundle
v1.64.0загружает fixture с passage без необработанного исключения в консоли; точный fallback и ограничение старого backend задокументированы. - В
scripts/mutation-gate.mjsзарегистрированы и проходят пять обязательных executable mutants из §15.6;--checkподтверждает уникальность их якорей. - 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:
- создать passage через toolbar на общей толстой стене;
- убедиться в preview без створки и в default 90 см;
- сохранить, hover/select, перетащить, undo/redo, удалить;
- создать дверь с contact+lock, выбрать passage, увидеть warning, Cancel;
- повторить и Save; убедиться, что bindings удалены и не восстановились;
- проверить Full View и Static в light/dark themes;
- включить Glow: внутренний проход светится насквозь, наружный — нет;
- проверить скрытую изометрию;
- full и space export/import;
- 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. План реализации
- Расширить types/schema и добавить чистый semantic validator со всеми call sites и backend tests.
- Расширить placement preset, toolbar, dialog draft/save canonicalization, warning и i18n.
- Добавить явные passage branches в 2D symbol/amount/locks/subscriptions.
- Подключить passage cut/tunnel к Static без изменения output старых типов.
- Сделать light classifier явным и покрыть interior/exterior cases.
- Добавить zero-leaf passage basis в скрытой изометрии.
- Покрыть import/export и исполняемый downgrade fixture на bundle
v1.64.0. - Зарегистрировать и исполнить пять mutation entries из §15.6.
- Обновить документацию и 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 не показывает регрессию.