From 91c8b01d7dba131382bc000c3b8e3a2b5d546e1b Mon Sep 17 00:00:00 2001 From: Sergey Matyunin Date: Sat, 15 Aug 2026 13:14:27 +0300 Subject: [PATCH] docs: specify open passage behavior Issue: #157 User-Visible: no --- docs/specs/157-open-passage.md | 627 +++++++++++++++++++++++++++++++++ docs/specs/README.md | 1 + 2 files changed, 628 insertions(+) create mode 100644 docs/specs/157-open-passage.md diff --git a/docs/specs/157-open-passage.md b/docs/specs/157-open-passage.md new file mode 100644 index 00000000..6ed5a4ad --- /dev/null +++ b/docs/specs/157-open-passage.md @@ -0,0 +1,627 @@ +# #157 — тип проёма «Открытый проём» + +- Issue: [#157](https://github.com/Matysh/houseplan-card/issues/157) +- Приоритет: P2 +- Ветка: `issue/157-open-passage` +- Статус документа: полное ТЗ подготовлено; issue остаётся в `S3-spec`, review не запущено по прямому указанию владельца +- Основание: issue владельца от 2026-08-14 и аналитика от 2026-08-15 + +## 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. +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`. Сообщение содержит id +пространства, id проёма и отсортированный список запрещённых полей, но не +значения entity ids. + +### 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; +- каждый кандидат проходит одинаковую проверку пола по обе стороны; +- только прошедшие кандидаты вычитаются из 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`. Реализация +должна подтвердить отсутствие runtime crash source-аудитом/fixture-тестом +pre-feature поведения; поддержка редактирования на старой версии не требуется. + +## 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. Старый frontend v1.64.0 на passage не падает; точный fallback и ограничение + старого backend задокументированы. +14. 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; +- 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. загрузить downgrade fixture старым frontend и подтвердить отсутствие crash. + +### 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. + +## 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. +8. Обновить документацию и 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 и поток редактора; +- `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 | +| 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` — рабочая icon; при отсутствии в поставляемой версии MDI допустим + локализованно нейтральный `mdi:door-open` без изменения persisted contract; +- passage расположен между door и gate в submenu; +- 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 не + показывает регрессию. diff --git a/docs/specs/README.md b/docs/specs/README.md index 566a282d..2749fb8b 100644 --- a/docs/specs/README.md +++ b/docs/specs/README.md @@ -89,6 +89,7 @@ GitHub Issues и GitHub Projects (v2) остаются единственным | [#123](https://github.com/Matysh/houseplan-card/issues/123) Split из вершины не меняет наружную геометрию стен | [123-corner-split-wall.md](123-corner-split-wall.md) | | [#137](https://github.com/Matysh/houseplan-card/issues/137) Узлы и линии привязки в редакторе Плана | [137-plan-snap-overlay.md](137-plan-snap-overlay.md) | | [#141](https://github.com/Matysh/houseplan-card/issues/141) Бесшовные стыки перегородок и открытых контуров | [141-wall-junctions.md](141-wall-junctions.md) | +| [#157](https://github.com/Matysh/houseplan-card/issues/157) Тип проёма «Открытый проём» | [157-open-passage.md](157-open-passage.md) | ## Правило актуализации