mirror of
https://github.com/Matysh/houseplan-card
synced 2026-09-30 19:58:50 +00:00
444 lines
27 KiB
Markdown
444 lines
27 KiB
Markdown
# Issue #262 — повторное добавление entity после удаления родительского устройства
|
||
|
||
- Issue: [#262](https://github.com/Matysh/houseplan-card/issues/262)
|
||
- Ветка: `issue/262-readd-child-entity`
|
||
- Статус: реализовано, ожидает код-ревью
|
||
- Ревью ТЗ: [`docs/reviews/SPEC-REVIEW-262-r1.md`](../reviews/SPEC-REVIEW-262-r1.md),
|
||
зелёный вердикт
|
||
- Приоритет: `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.
|