From 563a850aaca0c2e6eae0a9a23e6878ca556396b4 Mon Sep 17 00:00:00 2001 From: Sergey Matyunin Date: Sat, 15 Aug 2026 03:46:43 +0300 Subject: [PATCH] docs: specify registryless opening entities Issue: #117 User-Visible: no --- docs/specs/117-registryless-opening-entity.md | 222 ++++++++++++++++++ docs/specs/README.md | 1 + 2 files changed, 223 insertions(+) create mode 100644 docs/specs/117-registryless-opening-entity.md diff --git a/docs/specs/117-registryless-opening-entity.md b/docs/specs/117-registryless-opening-entity.md new file mode 100644 index 00000000..e256efc2 --- /dev/null +++ b/docs/specs/117-registryless-opening-entity.md @@ -0,0 +1,222 @@ +# Issue #117 — registry-less YAML entity работает у проёма в View + +- **Issue:** https://github.com/Matysh/houseplan-card/issues/117 +- **Статус документа:** готово к будущей реализации; issue остаётся на `S3-spec` +- **Приоритет:** P2 +- **Тип:** bug, обычный трек +- **Пользовательское изменение:** да + +## 1. Проблема + +Picker проёма принимает точную live YAML entity без `unique_id`: у неё есть +`hass.states[entity_id]`, но нет Entity Registry row. Live availability уже +считает такую ссылку active. Immutable render path дополнительно требует +`_renderPlanHass.entities[entity_id]`, поэтому сохранённый contact/lock не влияет +на отрисованный проём. + +Это разрыв одного exact-binding contract между editor, action и painted frame. + +## 2. Установленная причина + +`activeRegistryHass()` намеренно сохраняет live states registry-less entities и +удаляет states только при явном disabled/orphaned registry evidence. Однако +`renderOpeningEntityAvailable()` требует одновременно: + +```ts +projectedHass.entities[entityId] && projectedHass.states[entityId] +``` + +У YAML entity первая часть всегда отсутствует. При этом `_openingAmt()` и lock +badges используют именно frame-local helper. Issue #104 сознательно оставил эту +часть отдельной и отметил parity как #117. + +## 3. Цели + +1. Если exact entity предлагается picker и имеет active live state, её contact + или lock работает в painted frame. +2. Сохранить запрет disabled/orphaned/missing entities. +3. Сохранить immutable render snapshot: ни один слой не читает raw live hass в + середине кадра. +4. Не связывать opening reference с marker lifecycle/tombstone. + +## 4. Не входит в задачу + +- добавление Entity Registry row или `unique_id`; +- изменение candidate picker; +- миграция `opening.contact`/`opening.lock`; +- marker lifecycle и removed bindings; +- изменение lock security/confirmation; +- generic поддержка registry-less entities во всех marker bindings; +- новые opening types или geometry. + +## 5. Каноническая availability policy + +### 5.1 Live path + +`openingEntityAvailable(hass, entityId, snapshot)` остаётся authoritative для +picker, info card и lock action. Exact opening reference active, когда +`resolveHaBindingStatus(...).kind === 'active'`. + +### 5.2 Render path + +`renderOpeningEntityAvailable(projectedHass, entityId)` принимает только +projection, построенную `activeRegistryHass()` и замороженную в общем render +snapshot. + +В такой projection наличие state является достаточным frame-local доказательством: + +```ts +return !!entityId && !!projectedHass.states?.[entityId]; +``` + +Отсутствующая registry row не является доказательством disabled. Явно disabled +entity/device и authoritative orphan не проходят, потому что +`activeRegistryHass()` удаляет их state до capture. + +Запрещено вызывать render helper с raw `this.hass`. Тип/имя параметра, +комментарий и source-contract test должны сохранять эту trust boundary. + +## 6. Матрица поведения + +| Entity | Live helper | Render helper | Результат | +| --- | --- | --- | --- | +| registry row active + state | active | active | работает | +| YAML/no registry row + live state | active | active | работает | +| registry entity `unavailable/unknown` | active | active | существующая unknown semantics | +| explicit `disabled_by` entity | inactive | state удалён | не работает | +| explicit disabled parent device | inactive | state удалён | не работает | +| authoritative orphan parent | inactive | state удалён | не работает | +| missing state | inactive/missing | inactive | не работает | +| limited registry + exact live state | active | active | работает | +| marker tombstone того же entity | не влияет | не влияет | opening работает | + +`unavailable/unknown` здесь означает, что exact reference существует; текущая +opening renderer semantics решает, рисовать closed/unknown и badge state. #117 +не подменяет эти states active/open. + +## 7. Render snapshot + +Capture уже добавляет `opening.contact` и `opening.lock` в `entityIds`. После +исправления необходимо доказать: + +- registry-less state входит в `_renderPlanHass.states`; +- один кадр использует одну и ту же state projection для leaf amount, tone, + lock badge и tooltip data; +- HA tick заменяет snapshot атомарно; +- config/registry update не смешивается с предыдущим state; +- static `houseplan-space-card` не получает новую интерактивность. + +Нельзя исправлять баг чтением `this.hass.states` непосредственно в +`_openingAmt()` или `_renderOpeningLocks()`. + +## 8. Contact contract + +Для door/window/gate с registry-less contact: + +- state проходит текущий `openingAmount(type, state, invert)`; +- invert сохраняется; +- open tone/leaf и opening info согласованы; +- неизвестный state не становится самовольно open; +- скрытие opening symbols не меняет физическую/light semantics, определённую + существующим opening contract. + +## 9. Lock contract и безопасность + +Registry-less `lock.*`: + +- показывает badge/state по той же матрице, что registry lock; +- info card использует live exact availability; +- lock/unlock action остаётся только в явно открытой info card; +- unlock по-прежнему требует confirmation; +- перед service call выполняется live availability check; +- tap по plan badge/opening не получает новую actuation семантику. + +#117 не ослабляет secure-device rules универсального toggle. + +## 10. Совместимость + +- config/storage/API не меняются; +- существующие registry-backed openings pixel-identical; +- disabled/missing cases не становятся видимыми; +- Plan/View/kiosk touch gestures и hit targets не меняются; +- no new i18n key; +- backend не меняется. + +## 11. Acceptance criteria + +1. Registry-less live contact, выбранный picker, меняет opening presentation. +2. Registry-less live lock показывает корректный badge/info state. +3. Full-card frame использует immutable active projection, не raw hass. +4. Active registry entity сохраняет прежнее поведение. +5. Disabled entity/device, authoritative orphan и missing state остаются + unavailable. +6. Limited-registry live exact entity работает. +7. Marker tombstone не блокирует independent opening reference. +8. Lock action security и unlock confirmation не меняются. +9. Contact/lock state update не создаёт geometry/config rebuild. +10. Existing opening golden и interactions не регрессируют. + +## 12. План тестирования + +### Unit + +- расширить `ha-binding-status.test.mjs` для render helper: + registry-backed, registry-less, disabled entity, disabled parent, orphan, + missing state, limited registry; +- mutation: вернуть requirement `.entities[entityId]` — YAML case красный; +- `openingAmount` registry-less state/invert parity. + +### Browser smoke + +- YAML-like `binary_sensor` без registry row: closed → open → closed; +- YAML-like `lock`: locked/unlocked/unknown badge и info; +- disabled row со stale live state не отображается; +- same entity tombstoned as marker, opening всё ещё работает; +- render snapshot atomicity на state tick; +- no service call from direct opening/badge tap; explicit info action unchanged. + +### Регрессия + +- existing opening/contact/lock smokes; +- `test/render-device-snapshot.test.mjs`; +- typecheck, full unit и build. + +Golden обновлять не нужно: registry-backed baseline не меняется. Новый YAML case +проверяется targeted smoke/DOM state; screenshot допустим как новое доказательство, +но не требует переакцептации старых сцен. + +## 13. План реализации + +1. Исправить frame-local helper и его trust-boundary comment. +2. Добавить unit matrix и mutation guard. +3. Добавить registry-less contact/lock smoke. +4. Проверить snapshot/action parity и regression suite. + +## 14. Документация и release-артефакты + +- оба changelog получают user-visible bug-fix пункт; +- `docs/USER-GUIDE.ru.md`/diagnostics уточняет, что exact YAML entity без + `unique_id` поддерживается у contact/lock; +- `docs/ARCHITECTURE.md` или HA binding doc фиксирует render projection rule; +- новых RU/EN строк нет; +- performance/golden/full backend harness не требуются; +- targeted browser smoke обязателен перед S7 по AC. + +## 15. Риски и откат + +| Риск | Мера | +| --- | --- | +| Raw hass обходит immutable frame | helper принимает только projection + source test | +| Disabled state случайно оживает | complete disabled/orphan matrix | +| Lock security расширяется | отдельные render/live action assertions | +| Marker tombstone снова влияет | independent-reference unit/smoke | + +Откат возвращает прежний render helper. Данные не мигрируют; сохранённые exact +references остаются в config. + +## 16. Принятые технические предположения + +- `activeRegistryHass()` остаётся единственным producer render projection; +- наличие state в этой projection достаточно для exact opening reference; +- unavailable/unknown считаются существующей entity, но не автоматически open; +- `houseplan-space-card` остаётся статическим и не получает actions. diff --git a/docs/specs/README.md b/docs/specs/README.md index 219f26f3..254116bb 100644 --- a/docs/specs/README.md +++ b/docs/specs/README.md @@ -86,6 +86,7 @@ GitHub Issues и GitHub Projects (v2) остаются единственным | [#94](https://github.com/Matysh/houseplan-card/issues/94) Универсальное действие «Переключить состояние» | [094-universal-state-toggle.md](094-universal-state-toggle.md) | | [#101](https://github.com/Matysh/houseplan-card/issues/101) Плавный переход View ↔ редакторы | [101-view-editor-transition.md](101-view-editor-transition.md) | | [#107](https://github.com/Matysh/houseplan-card/issues/107) Переключение виртуального источника света «Всегда» | [107-virtual-light-toggle.md](107-virtual-light-toggle.md) | +| [#117](https://github.com/Matysh/houseplan-card/issues/117) Registry-less entity у проёма | [117-registryless-opening-entity.md](117-registryless-opening-entity.md) | | [#122](https://github.com/Matysh/houseplan-card/issues/122) Изометрический режим Stage 2: скрытый режим и визуальная полировка | [122-isometric-stage2.md](122-isometric-stage2.md) | | [#123](https://github.com/Matysh/houseplan-card/issues/123) Split из вершины не меняет наружную геометрию стен | [123-corner-split-wall.md](123-corner-split-wall.md) | | [#132](https://github.com/Matysh/houseplan-card/issues/132) Проёмы в независимых стенах (+ bug [#185](https://github.com/Matysh/houseplan-card/issues/185)) | [132-partition-openings.md](132-partition-openings.md) |