docs: specify registryless opening entities

Issue: #117
User-Visible: no
This commit is contained in:
Sergey Matyunin
2026-08-19 02:11:35 +03:00
parent 9e5ff0b8a0
commit 563a850aac
2 changed files with 223 additions and 0 deletions
@@ -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.
+1
View File
@@ -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) |