27 KiB
Issue #262 — повторное добавление entity после удаления родительского устройства
- Issue: #262
- Ветка:
issue/262-readd-child-entity - Статус: реализовано, ожидает код-ревью
- Ревью ТЗ:
docs/reviews/SPEC-REVIEW-262-r1.md, зелёный вердикт - Приоритет:
P2 - Тип:
bug - Трек: обычный
- Связанные задачи: #161, #226, #263, #104
1. Сценарий и персона
Персона: администратор дома из docs/SCOPE.md, который поддерживает план в
актуальном состоянии (J6).
Поверхность: desktop-first редактор устройств, диалог Добавить → выбор HA-привязки → включённый флаг Показывать сущности.
Сценарий:
- на плане размещено HA-устройство
device:Dс несколькими дочерними сущностями; - пользователь удаляет устройство с плана, и House Plan сохраняет минимальный
device:Dtombstone; - позже пользователь хочет вернуть не всё устройство, а только одну его
сущность
entity:X; - пользователь открывает Добавить, включает Показывать сущности, находит 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:Dtombstone останется;buildDevices()снова вызоветisRemovedPlanEntity()и отбросит уже живой explicit marker X;- plan/render availability, live text, room-source picker и агрегаты используют ту же модель удаления и также могут считать X недоступной.
Следовательно, контракт должен охватывать и вход в picker, и runtime-приоритет живого exact entity-marker над родительским tombstone.
5. Scope
Входит
- Показ активных допустимых дочерних entity удалённого
device:Dв Add при включённом Показывать сущности. - Сохранение выбранной
entity:Xкак обычного живого explicit marker. - Сохранение
device:Dtombstone после такого действия: родительское устройство не должно автоматически воскреснуть. - Exact live
entity:Xимеет приоритет над tombstone самой X и/или её parent D во всех обычных marker-level runtime-потребителях. - Все не выбранные siblings устройства D и device-level binding D остаются удалёнными с плана.
- Согласованность с ownership-контрактом #226 и существующими правилами exact re-add из #161.
- Unit, browser smoke, mutation guard, документация и changelog.
Не входит
- Автоматическое включение Показывать сущности или изменение его текста.
- Снятие лимита 200 строк и изменение поиска/сортировки picker.
- Показ HA-disabled, orphaned, registry-hidden или недоказанных entity: для них продолжают действовать текущие registry-фильтры.
- Автоматическое восстановление всех дочерних entity при выборе одной.
- Изменение независимого контракта opening contact/lock из #104: точные ссылки архитектурных объектов по-прежнему игнорируют marker tombstones.
- Очистка tombstones, layout или файлов через Optimize.
- Новая миграция формата конфига или изменение
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:Dtombstone; - ровно один живой marker с binding
entity:X; - ни одного
entity:Xtombstone; - без изменений tombstones/markers остальных binding.
Новый X получает обычную свежую позицию по действующему правилу Add. Позиция, метаданные и файлы удалённого D не восстанавливаются и не наследуются X.
Если конфиг содержит старый/конкурентный exact entity:X tombstone, сохранение X
заменяет его как обычный exact re-add. Parent tombstone при этом сохраняется.
6.3 Runtime-приоритет
Для marker-level семантики действует порядок:
- живой explicit
entity:Xразрешает X; - exact
entity:Xtombstone подавляет X, если живого exact marker нет; device:Dtombstone подавляет X, если живого exact marker нет;- обычная 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 Дальнейшие действия
- Если пользователь снова удаляет X, рядом с parent tombstone появляется exact
entity:Xtombstone; X снова доступна для exact re-add через picker. - Если пользователь затем явно добавляет
device:D, exact device tombstone заменяется живым device-marker. Живой X сохраняется рядом: два явных marker считаются осознанной конфигурацией по #226. - Если пользователь добавляет несколько дочерних 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и liveentity:*marker. - Новых полей и версии модели нет; read/write migration не нужна.
- Комбинация
device:D removed:true+ liveentity: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:Dsource остаётся удалённым;- 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
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:Dsource remains suppressed in every case with parent tombstone.
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.
- Room/source consumers include X but not Y.
- Delete/re-add record helper preserves unrelated tombstones and produces one exact live binding.
Browser smoke
Расширить demo/smoke_binding_picker.mjs полноценным lifecycle вместо одной
проверки текущего дефекта:
- удалить реальное устройство стенда;
- открыть Add с checkbox off/on и проверить X;
- сохранить X через
_saveMarker(); - проверить records,
_devices, DOM и свежую layout position; - проверить, что D по-прежнему предлагается, а X уже не дублируется;
- удалить и повторно добавить X;
- вернуть 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: yesimplementation 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. Принятые предположения — поменять свободно на ревью ТЗ
- Parent
device:Dtombstone сохраняется после re-add X. Это следует из пользовательского действия «вернуть одну entity», а также предотвращает автоматическое появление нежелательного остаточного parent marker. - Live exact
entity:Xсильнее exact/parent tombstone только для X. Техническая форма (дополнительный Set в prepared policy, отдельный helper или иной O(1) resolver) не является продуктовым решением. - Registry-hidden/disabled entity не получает нового специального whitelist: issue исправляет только plan tombstone, а не HA availability.
- Fresh position X определяется существующим Add-контрактом; отдельной миграции координат D → X нет.
- Полноценное добавление D после X оставляет оба explicit marker по уже утверждённому #226; скрытое автоматическое объединение было бы новым UX.