docs: specify child entity re-add lifecycle

Issue: #262
User-Visible: no
This commit is contained in:
Sergey Matyunin
2026-08-23 16:19:48 +03:00
parent 98dce8d0b4
commit 7b3ea3769a
2 changed files with 443 additions and 0 deletions
@@ -0,0 +1,442 @@
# Issue #262 — повторное добавление entity после удаления родительского устройства
- Issue: [#262](https://github.com/Matysh/houseplan-card/issues/262)
- Ветка: `issue/262-readd-child-entity`
- Статус: `S3-spec`
- Приоритет: `P2`
- Тип: `bug`
- Трек: обычный
- Связанные задачи: [#161](https://github.com/Matysh/houseplan-card/issues/161),
[#226](https://github.com/Matysh/houseplan-card/issues/226),
[#263](https://github.com/Matysh/houseplan-card/issues/263),
[#104](https://github.com/Matysh/houseplan-card/issues/104)
## 1. Сценарий и персона
**Персона:** администратор дома из `docs/SCOPE.md`, который поддерживает план в
актуальном состоянии (J6).
**Поверхность:** desktop-first редактор устройств, диалог **Добавить** → выбор
HA-привязки → включённый флаг **Показывать сущности**.
**Сценарий:**
1. на плане размещено HA-устройство `device:D` с несколькими дочерними
сущностями;
2. пользователь удаляет устройство с плана, и House Plan сохраняет минимальный
`device:D` tombstone;
3. позже пользователь хочет вернуть не всё устройство, а только одну его
сущность `entity:X`;
4. пользователь открывает **Добавить**, включает **Показывать сущности**, находит
X, сохраняет маркер и размещает его как обычную отдельную entity.
Это не сценарий администрирования реестра HA: устройство и X остаются активными
в Home Assistant. Меняется только состав объектов House Plan.
## 2. Что человек увидит до и после
**До:** удалённое устройство можно вернуть целиком, но ни одну его дочернюю
сущность нельзя найти даже при включённом **Показывать сущности**.
**После:** в том же списке доступны активные дочерние сущности удалённого
устройства; сохранение выбранной сущности возвращает только её, а удалённое
родительское устройство и остальные его сущности сами на план не возвращаются.
Новых кнопок, предупреждений, toast-сообщений и настроек не появляется.
## 3. Подтверждённое воспроизведение
Текущий браузерный smoke `demo/smoke_binding_picker.mjs`, добавленный в #263,
проверяет настоящий `_bindingCandidates()` собранной карточки и фиксирует:
- `device:D` после удаления снова предлагается — работает;
- обычная entity другого живого устройства появляется после включения
**Показывать сущности** — работает;
- `entity:X` удалённого `device:D` отсутствует —
`knownDefect262ChildEntityBlocked: true`.
Автор отчёта подтвердил именно этот третий сценарий. Повторное добавление всего
устройства является обходом, но меняет желаемый состав маркера и потому не
закрывает проблему.
## 4. Причина
### 4.1 Picker разрешает только точный tombstone binding
`removedPlanBindings()` хранит отдельно удалённые device ids и entity ids.
`isRemovedPlanEntity(hass, X, removed)` считает X удалённой, когда удалена либо
сама `entity:X`, либо её родитель `device:D`.
`_bindingCandidates()` допускает исключение для точной строки из набора
`removedBindings`:
- tombstone `device:D` разрешает снова показать `device:D`;
- tombstone `entity:X` разрешает снова показать `entity:X`;
- tombstone `device:D` **не** является точной строкой `entity:X`, поэтому X
отбрасывается общим `isRemovedPlanEntity()`.
### 4.2 Исправления только списка недостаточно
Если принудительно записать `entity:X` поверх текущего конфига:
- `_saveMarker()` удалит только tombstone с тем же exact binding; родительский
`device:D` tombstone останется;
- `buildDevices()` снова вызовет `isRemovedPlanEntity()` и отбросит уже живой
explicit marker X;
- plan/render availability, live text, room-source picker и агрегаты используют
ту же модель удаления и также могут считать X недоступной.
Следовательно, контракт должен охватывать и вход в picker, и runtime-приоритет
живого exact entity-marker над родительским tombstone.
## 5. Scope
### Входит
1. Показ активных допустимых дочерних entity удалённого `device:D` в Add при
включённом **Показывать сущности**.
2. Сохранение выбранной `entity:X` как обычного живого explicit marker.
3. Сохранение `device:D` tombstone после такого действия: родительское устройство
не должно автоматически воскреснуть.
4. Exact live `entity:X` имеет приоритет над tombstone самой X и/или её parent D
во всех обычных marker-level runtime-потребителях.
5. Все не выбранные siblings устройства D и device-level binding D остаются
удалёнными с плана.
6. Согласованность с ownership-контрактом #226 и существующими правилами exact
re-add из #161.
7. Unit, browser smoke, mutation guard, документация и changelog.
### Не входит
1. Автоматическое включение **Показывать сущности** или изменение его текста.
2. Снятие лимита 200 строк и изменение поиска/сортировки picker.
3. Показ HA-disabled, orphaned, registry-hidden или недоказанных entity: для них
продолжают действовать текущие registry-фильтры.
4. Автоматическое восстановление всех дочерних entity при выборе одной.
5. Изменение независимого контракта opening contact/lock из #104: точные ссылки
архитектурных объектов по-прежнему игнорируют marker tombstones.
6. Очистка tombstones, layout или файлов через Optimize.
7. Новая миграция формата конфига или изменение `PLAN_MODEL_VERSION`.
## 6. Контракт поведения
### 6.1 Матрица Add picker
Пусть активная registry-backed `entity:X` принадлежит активному `device:D`.
| Конфиг House Plan | `device:D` в Add | `entity:X` при выключенном флаге | `entity:X` при включённом флаге |
|---|---:|---:|---:|
| ничего не размещено/не удалено | по текущим правилам | нет | да |
| живой `device:D` marker | нет | нет | да, если exact X не занята |
| `device:D` tombstone | да | нет | **да** |
| `entity:X` tombstone, parent жив | по текущим правилам | нет | да |
| живой `entity:X` marker | по текущим правилам #226 | нет | нет |
Флаг **Показывать сущности** остаётся выключенным для нового маркера. Исправление
не маскирует эту существующую UX-ступень.
### 6.2 Транзакция повторного добавления
При сохранении X из состояния с `device:D` tombstone итоговый persisted набор
содержит:
- прежний минимальный `device:D` tombstone;
- ровно один живой marker с binding `entity:X`;
- ни одного `entity:X` tombstone;
- без изменений tombstones/markers остальных binding.
Новый X получает обычную свежую позицию по действующему правилу Add. Позиция,
метаданные и файлы удалённого D не восстанавливаются и не наследуются X.
Если конфиг содержит старый/конкурентный exact `entity:X` tombstone, сохранение X
заменяет его как обычный exact re-add. Parent tombstone при этом сохраняется.
### 6.3 Runtime-приоритет
Для marker-level семантики действует порядок:
1. живой explicit `entity:X` разрешает X;
2. exact `entity:X` tombstone подавляет X, если живого exact marker нет;
3. `device:D` tombstone подавляет X, если живого exact marker нет;
4. обычная registry availability решает, активна ли разрешённая X в HA.
Исключение относится только к exact живой entity-binding. Нельзя трактовать
наличие **одной** восстановленной entity как снятие parent tombstone со всех
siblings.
Разрешённая X должна вести себя как обычный entity-marker во всех потребителях:
- строиться и рендериться во View, kiosk, editors и static card;
- получать live state, value badge, activity, Glow и разрешённые действия по
существующим правилам домена;
- участвовать в room aggregates/source pickers там, где обычная explicit X
участвовала бы без tombstone;
- быть доступной live text и marker controls, если эти потребители ссылаются на
X по существующим правилам.
### 6.4 Что остаётся удалённым
После восстановления X:
- `device:D` не строится автоматически, потому что его tombstone остаётся
точным claim родительского binding;
- `device:D` остаётся доступным в Add для осознанного восстановления целиком;
- любая sibling `entity:Y`, не имеющая собственного живого explicit marker,
остаётся подавленной во всех marker-level runtime-потребителях;
- device-level source `device:D` остаётся удалённым;
- автоматические агрегаты не получают данные Y через ослабленный общий guard.
### 6.5 Дальнейшие действия
1. Если пользователь снова удаляет X, рядом с parent tombstone появляется exact
`entity:X` tombstone; X снова доступна для exact re-add через picker.
2. Если пользователь затем явно добавляет `device:D`, exact device tombstone
заменяется живым device-marker. Живой X сохраняется рядом: два явных marker
считаются осознанной конфигурацией по #226.
3. Если пользователь добавляет несколько дочерних entity D, каждая становится
отдельным explicit marker; parent tombstone остаётся один.
## 7. UX, touch и accessibility
- UI и последовательность действий не меняются.
- Desktop-first контракт Device editor сохраняется.
- Touch-поведение редактора не расширяется и не ухудшается: новых pointer
targets/gestures нет.
- Тексты и accessibility tree не меняются.
- View/kiosk получают только ожидаемое возвращение живого marker после save;
редакторские элементы в них не появляются.
## 8. Данные, compatibility и миграция
- Используются существующие records `marker.removed` и live `entity:*` marker.
- Новых полей и версии модели нет; read/write migration не нужна.
- Комбинация `device:D removed:true` + live `entity:X` уже schema-valid и должна
round-trip без нормализации, удаляющей один из records.
- Старый frontend, открытый одновременно после записи нового сочетания, может
временно продолжать скрывать X по старой runtime-семантике. Он не должен
разрушительно удалить X или parent tombstone; после обновления/reload новый
frontend показывает X.
- Delete/re-add остаются optimistic config-транзакциями с действующими rev
guards; задача не вводит отдельную запись или частичный commit.
- Layout identity X определяется текущим `markerIdForBinding()`; parent D layout
не переиспользуется.
## 9. i18n, security и performance
### i18n
Новых ключей и текстов нет. Документация EN/RU обновляется симметрично.
### Security
Исправление не меняет resolver действий и lock invariant. Разрешённая X получает
ровно те же safe/secure ограничения, что обычный explicit entity-marker.
### Performance
- Не добавляется проход по HA registry на render/update.
- Допустима линейная подготовка небольшого набора exact live entity bindings
вместе с уже существующим проходом по `markers[]`.
- Проверка одного eid должна оставаться O(1) по подготовленным Set.
- Перестроение `_devices` и render snapshots не должно создавать новый объект
состояния на каждый вызов availability сверх текущей модели.
## 10. Acceptance criteria и доказательства
### AC1. Дочерняя entity доступна в Add
После удаления `device:D` активная дочерняя `entity:X` отсутствует при
выключенном **Показывать сущности** и присутствует при включённом. Сам `device:D`
также остаётся доступным для re-add; размещённые bindings не дублируются.
**Доказательство:** браузерный `demo/smoke_binding_picker.mjs` вызывает настоящий
picker собранного бандла. Проверка known defect перевёрнута в положительную и
проверяет checkbox boundary.
### AC2. Сохранение возвращает только выбранную entity
После save X persisted config содержит live `entity:X` и прежний `device:D`
tombstone; X видна на плане с новой обычной позицией, parent auto-marker не
появляется. Повторный delete/re-add X идемпотентен.
**Доказательство:** browser smoke проходит delete D → Add X → Save → rebuild →
Delete X → Add X → Save и проверяет config records, layout и DOM/device list.
Targeted unit покрывает чистую семантику tombstone/live-binding.
### AC3. Исключение exact и не протекает на siblings
При `device:D` tombstone + live `entity:X`:
- X доступна plan/render availability и обычным state/action/aggregate
потребителям;
- sibling Y без live marker остаётся удалённой;
- `device:D` source остаётся удалённым;
- parent auto-marker отсутствует.
**Доказательство:** unit-матрица для `isRemovedPlanEntity`, `isRemovedPlanSource`,
`buildDevices`, room aggregate и explicit controls/live source; browser smoke
проверяет видимый X и отсутствие D/Y.
### AC4. Существующие lifecycle-контракты не меняются
- exact re-add `device:D` из #161 продолжает заменять device tombstone;
- exact re-add `entity:X` продолжает заменять entity tombstone;
- entity tombstone не вычитает X из живого auto-parent;
- два живых explicit `device:D` + `entity:X` сосуществуют по #226;
- exact opening contact/lock из #104 не зависит от marker tombstones.
**Доказательство:** существующие unit/smoke #104/#161/#226 плюс новые regression
assertions в picker smoke.
### AC5. Данные и UX совместимы
Нет новых config/i18n полей, model bump или миграции. Checkbox, поиск, лимит 200,
disabled/hidden registry-фильтры, touch contract и lock safety не меняются.
**Доказательство:** schema/config round-trip unit, diff review, typecheck и docs
check; новых i18n keys нет.
### AC6. Гейты реализации зелёные
- `npm run typecheck`;
- `npm test`;
- `npm run build` и byte-identical три bundle-копии;
- targeted browser smokes, выбранные `smoke-select`;
- named mutation guards §11;
- `node scripts/check-docs.mjs`.
Полные golden, smoke и performance остаются предрелизными гейтами. Golden не
требуется для этого невизуального изменения, если diff не меняет визуальные
baseline-сценарии.
## 11. План автотестов
### Unit
1. `removedPlanBindings`/availability matrix:
- parent tombstone без live child → X/Y suppressed;
- parent tombstone + live X → X allowed, Y suppressed;
- exact X tombstone без live X → X suppressed;
- legacy duplicate exact tombstone + live X → live exact marker wins;
- `device:D` source remains suppressed in every case with parent tombstone.
2. `buildDevices`:
- only X is built from parent tombstone + live X;
- D does not auto-build;
- adding live explicit D later yields D + X as #226 requires.
3. Room/source consumers include X but not Y.
4. Delete/re-add record helper preserves unrelated tombstones and produces one
exact live binding.
### Browser smoke
Расширить `demo/smoke_binding_picker.mjs` полноценным lifecycle вместо одной
проверки текущего дефекта:
1. удалить реальное устройство стенда;
2. открыть Add с checkbox off/on и проверить X;
3. сохранить X через `_saveMarker()`;
4. проверить records, `_devices`, DOM и свежую layout position;
5. проверить, что D по-прежнему предлагается, а X уже не дублируется;
6. удалить и повторно добавить X;
7. вернуть D и подтвердить осознанное сосуществование D + X.
`scripts/smoke-links.mjs` должен выбирать smoke при изменении helpers tombstone
и нового exact-live override. Если имя `_bindingCandidates` остаётся прямым
совпадением, дублировать его зарегистрированной связью не требуется.
### Mutation guards
| id | Мутант | Обязанный поймать тест |
|---|---|---|
| `device-tombstone-blocks-child-picker` | вернуть общий запрет X в picker при parent tombstone | AC1 browser smoke |
| `live-child-still-suppressed-by-parent-tombstone` | удалить exact live override из runtime | AC2/AC3 unit + smoke |
| `child-readd-clears-parent-tombstone` | при save X удалить `device:D` tombstone | AC2 smoke/config assertion |
| `parent-tombstone-restores-all-siblings` | разрешить все entity D вместо только живых explicit bindings | AC3 unit |
Перед добавлением мутанты запускаются и должны быть реально пойманы. Непроверенный
mutation id в репозиторий не добавляется.
## 12. Затронутые файлы и артефакты
Ожидаемый продуктовый diff:
- `src/devices.ts` — представление tombstones и exact live override;
- `src/houseplan-card.ts` — Add candidates и, если нужно, атомарная замена exact
entity tombstone без parent tombstone;
- `test/devices.test.mjs` и/или узкий policy unit;
- `demo/smoke_binding_picker.mjs`;
- `scripts/smoke-links.mjs`, `scripts/mutation-gate.mjs`;
- `docs/FILTERING.md`, `docs/ARCHITECTURE.md`, `docs/TESTING.md`;
- `docs/USER-GUIDE.md`, `docs/USER-GUIDE.ru.md`;
- `docs/CHANGELOG.md`, `docs/CHANGELOG.ru.md`;
- три синхронные bundle-копии после build.
Если реализация требует новых config fields, i18n keys или backend-файлов, это
выход за утверждённый scope и повод вернуть ТЗ на уточнение до продолжения кода.
## 13. Release artifacts
- EN/RU changelog в том же `User-Visible: yes` implementation commit;
- EN/RU user guide и канонические filtering/architecture/testing docs;
- bundle parity;
- handoff-комментарий с commit SHA, точными targeted gates и остаточными рисками;
- независимый `CODE-REVIEW-262-rN.md` до автоматического merge в `dev`.
Публичные screenshots не меняются: новых визуальных состояний и UI нет.
## 14. Риски и меры снижения
### R1. Воскреснет весь parent device
Если save X удалит `device:D` tombstone, auto-discovery построит D из остаточных
siblings. Защита: persisted-record и DOM assertions AC2/AC3.
### R2. Исключение протечёт на siblings и агрегаты
Если parent tombstone перестанет подавлять device целиком, удалённые Y снова
начнут влиять на room climate/LQI/Glow или controls. Защита: exact X/Y unit
матрица и mutation `parent-tombstone-restores-all-siblings`.
### R3. X появится в picker, но останется мёртвой после Save
Изолированная UI-правка сделает выбор возможным, однако `buildDevices` и
availability снова отбросят marker. Защита: smoke обязан пройти до DOM/live
state после `_saveMarker()`, а не завершаться на списке candidates.
### R4. Сломается #226
Нельзя считать entity tombstone живым ownership или запретить явное
сосуществование D + X. Защита: существующая unit/smoke-матрица #226 входит в
обязательную регрессию AC4.
### R5. Старый frontend увидит новое сочетание иначе
Старый runtime может временно скрыть X рядом с parent tombstone. Поскольку
records schema-valid и не удаляются старой версией, это обратимая визуальная
деградация до reload новой сборки, а не потеря данных.
## 15. Откат
Откат — один implementation commit #262 вместе с unit/smoke/mutation,
документацией, changelog и bundle-копиями.
Новых persisted fields нет, поэтому data rollback не требуется. После отката
конфиг `device:D removed:true` + live `entity:X` остаётся валидным и сохранным,
но старая версия снова временно не покажет X, пока пользователь не вернёт D
целиком или не установит исправленную сборку.
## 16. Принятые предположения — поменять свободно на ревью ТЗ
1. Parent `device:D` tombstone сохраняется после re-add X. Это следует из
пользовательского действия «вернуть одну entity», а также предотвращает
автоматическое появление нежелательного остаточного parent marker.
2. Live exact `entity:X` сильнее exact/parent tombstone только для X. Техническая
форма (дополнительный Set в prepared policy, отдельный helper или иной O(1)
resolver) не является продуктовым решением.
3. Registry-hidden/disabled entity не получает нового специального whitelist:
issue исправляет только plan tombstone, а не HA availability.
4. Fresh position X определяется существующим Add-контрактом; отдельной миграции
координат D → X нет.
5. Полноценное добавление D после X оставляет оба explicit marker по уже
утверждённому #226; скрытое автоматическое объединение было бы новым UX.
+1
View File
@@ -131,6 +131,7 @@ GitHub Issues и GitHub Projects (v2) остаются единственным
| [#252](https://github.com/Matysh/houseplan-card/issues/252) Понятная и безопасная очистка забытых позиций в Optimize | [252-optimize-orphan-layout-report.md](252-optimize-orphan-layout-report.md) |
| [#253](https://github.com/Matysh/houseplan-card/issues/253) Resize не теряет интервалы толщины стен | [253-resize-wall-thickness.md](253-resize-wall-thickness.md) |
| [#258](https://github.com/Matysh/houseplan-card/issues/258) Канонический wall key после Optimize и storage round-trip | [258-wall-key-storage-roundtrip.md](258-wall-key-storage-roundtrip.md) |
| [#262](https://github.com/Matysh/houseplan-card/issues/262) Повторное добавление entity после удаления родительского устройства | [262-readd-child-entity-after-device-delete.md](262-readd-child-entity-after-device-delete.md) |
## P3