Files
houseplan-card/docs/specs/262-readd-child-entity-after-device-delete.md
T
2026-08-23 16:41:15 +03:00

27 KiB
Raw Blame History

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-привязки → включённый флаг Показывать сущности.

Сценарий:

  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.