# CODE-REVIEW-262-r1 - Issue: [#262](https://github.com/Matysh/houseplan-card/issues/262) — «Deleting entities prevents them from being added again later» - Ветка: `issue/262-readd-child-entity` - HEAD ревью: `5fa2dc289354f83b3e0e36126cd115fcfcc3b5ac` (совпадает с точным HEAD, указанным автором в хендоффе) - Реализация: `8cdf6b489481badaf25817efcca6cff1079ef6d1` · `Issue: #262` · `User-Visible: yes` - ТЗ: `docs/specs/262-readd-child-entity-after-device-delete.md`, ревью ТЗ зелёное (`docs/reviews/SPEC-REVIEW-262-r1.md`) - Заход: **r1** · блокирующих циклов израсходовано **0/4** ## Скоуп Точный tombstone-binding в picker и runtime-семантике блокировал повторное добавление ОДНОЙ дочерней entity удалённого устройства, хотя само устройство целиком возвращалось штатно. Правка вводит единственное исключение: живой exact `entity:X` marker перекрывает parent `device:D` tombstone только для X, не трогая siblings, device-level source и существующие контракты #161/#226/#104. Продуктовый diff: - `src/devices.ts` — `RemovedPlanBindings.liveEntities`, `isRemovedPlanEntity()`, `areaClimateMap()`; - `src/houseplan-card.ts` — `_bindingCandidates()` (picker-исключение для детей tombstoned device за флагом «Показывать сущности»). Плюс `test/devices.test.mjs` (+3 теста), `demo/smoke_binding_picker.mjs` (known-defect-проверка перевёрнута и расширена до полного lifecycle), `scripts/mutation-gate.mjs` (+4 мутанта), документация (`FILTERING.md`, `ARCHITECTURE.md`, `TESTING.md`, `USER-GUIDE.md`/`.ru.md`, оба CHANGELOG), пересъёмка одного скриншота и `screenshots.json`. ## Как проверялось | Гейт | Команда | Результат | |---|---|---| | typecheck | `npx tsc --noEmit` | green, без вывода | | unit | `npm test` | 1162/1162 pass, 0 fail (0 skip здесь; у автора 1 skip — `process-gate.test.mjs` пропускает git/gh-зависимые кейсы в зависимости от окружения, не связано с диффом) | | build + bundle parity | `npm run build && npm run bundle:sync` + `sha256sum` трёх копий | все три идентичны, `f88f5089…f65aff9d` — совпадает с SHA из хендоффа | | docs fingerprint | `node scripts/check-docs.mjs` | `Documentation checks passed (7 files, 10 external links)` | | smoke-select | `node scripts/smoke-select.mjs --base origin/dev --head HEAD` | одна зарегистрированная связь: `demo/smoke_binding_picker.mjs` ← `isRemovedPlanEntity` | | целевой browser smoke | `node demo/smoke_binding_picker.mjs` | 24/24 проверки true, `OK` | | mutation guards (4 именованных) | `node scripts/mutation-gate.mjs --id=<каждый>` | все 4 — «тест покраснел, как обязан», поймано 1/1 каждый | | process-gate (офлайн) | `node scripts/process-gate.mjs --base origin/dev --head HEAD` | «гейт пройден, предупреждений 0» | | CI на точном SHA | `gh run view` | `Validate` на `5fa2dc2…` — `success`; `Docs screenshots` на `8cdf6b4…` — `success` (канонический Linux-артефакт, принятие скриншота обосновано) | ### Чего не проверял и почему - `npm run golden:verify` — не запускал. Diff не меняет рендер/геометрию/стили; единственный визуальный артефакт (`docs/images/09-device-info.png`) обновлён только из-за отпечатка `src/**`, что подтверждено принятым прогоном `Docs screenshots` на коммите реализации, а не новым UI-состоянием. - `python -m pytest tests_backend` — не запускал. Диф не трогает `custom_components/**/*.py` (подтверждено `git diff --stat`). - `node scripts/model-invariants.mjs` — не запускал. Diff не трогает геометрию, `layout`, `marker.space`, `open_spans`, ребра комнат — только tombstone/entity runtime-семантику. - performance-профили — не запускал; в AC они не названы, а сложность ограничена O(1)-проверкой по уже существующему проходу по `markers[]` (см. ниже). - Полный `demo/smoke_*.mjs` набор — не гонял. `smoke-select` даёт ровно одну зарегистрированную связь и diff локален для picker/tombstone-модели; остальные смоки (стены, геометрия, толщина, golden-инфраструктура) тематически не связаны с этим диффом. ## Находки Нет ни одной High- или Medium-находки в скоупе или вне скоупа. Единственное замечание вне скоупа (`docs/USER-GUIDE.ru.md` vs `marker.show_entities` i18n) было выявлено на этапе ревью ТЗ и корректно заведено отдельно как [#269](https://github.com/Matysh/houseplan-card/issues/269) (проверено: issue существует, `P3`/`docs`/`S1-new`, не тронут в этой ветке). Новый текст документации, добавленный в этом коммите, повторяет уже существующую (пусть и спорную) формулировку гайда без создания нового расхождения. ## AC — проверка **AC1. Дочерняя entity доступна в Add.** Доказано `demo/smoke_binding_picker.mjs`: `deletedDeviceChildHiddenWithoutCheckbox`/`deletedDeviceChildOfferedWithCheckbox` = true; `device:D` остаётся предложен (`parentStillOfferedAfterChildSave`). Прочитан код: picker-исключение (`childOfRemovedDevice`) находится строго внутри блока `if (this._markerDialog?.showEntities)`, поэтому флаг не обходится. Mutation `device-tombstone-blocks-child-picker` подтверждает, что тест падает при откате исключения. **AC2. Save возвращает только X.** Доказано smoke (`childSaveCreatesOneLiveMarker`, `childSaveKeepsParentTombstone`, `childBuiltWithoutAutoParent`, `childGetsFreshLayoutPosition`, `childDrawnInView`) + unit (`buildDevices: only X built from parent tombstone`). Идемпотентность delete→re-add проверена smoke (`childDeleteLeavesBothTombstones`, `secondChildSaveIsIdempotent`). Прочитан код `_saveMarker`: фильтр удаляет только маркеры с тем же `id` или тем же `binding`, что и сохраняемый — parent-tombstone с другим binding переживает save по построению; mutation `child-readd-clears-parent-tombstone` подтверждает, что тест ловит регрессию, если фильтр ослабить до полного сброса tombstones при сохранении entity-марки. **AC3. Исключение exact, не протекает на siblings.** Доказано unit (`isRemovedPlanEntity`/`isRemovedPlanSource` для X vs `switch.mic_mute` vs `device:hub`; `areaClimateMap` room climate только по X) + smoke (`liveChildRemovedFromPicker` для X после сохранения; отсутствие sibling/D в `_devices`). Прочитан код: единственная точка исключения — `removed.liveEntities.has(eid)` в `isRemovedPlanEntity`; все runtime-потребители (`_planEntityAvailable`, `_renderEntityAvailable`, `_roomSrcCandidates`, `buildDevices`, `isRemovedPlanSource`, `applyMarker`→controls) идут через эту же функцию или через `buildDevices`, то есть исключение — «один choke point», а не раздельные патчи с риском разойтись. Отдельно патчен только `areaClimateMap` — он читает `markers`/`removed.devices` напрямую, минуя `isRemovedPlanEntity`; грепом по `removed.(devices|entities).has` подтверждено, что других таких прямых обращений в `src/**`, кроме уже патченных мест и `_roomSrcCandidates` (там нужен именно device-level запрет, без исключения — верно по §6.4), не осталось. Mutations `live-child-still-suppressed-by-parent-tombstone` и `parent-tombstone-restores-all-siblings` подтверждают, что тест ловит и отсутствие исключения, и его чрезмерное расширение на всех siblings сразу. **AC4. Существующие контракты не меняются.** Доказано полным прогоном `npm test` (1162/1162, включает регресс-покрытие #161/#226/#104/#233/#234 и т.д. без дискриминации по имени) — из диффа видно, что тесты на #226 (`entity tombstone does not strip that entity from a live parent device`), #161 (readd) и opening contact/lock не переписывались. Прочитан код: `entityMarkerOwnership`/ `residualAutoDeviceEntities` (контракт #226) не тронуты; `isRemovedPlanEntity` и его новая ветка проверяются раньше существующих проверок и не меняют их семантику для случаев без live exact marker. **AC5. Данные и UX совместимы.** Диф не содержит новых config-полей, i18n-ключей или миграции (подтверждено `git diff --stat`: нет изменений в `src/i18n/*`, `custom_components/**/*.py`, `PLAN_MODEL_VERSION`). `npx tsc --noEmit` green. `check-docs` green. UI не меняется — читано: единственная правка в `houseplan-card.ts` — булево условие внутри уже существующего цикла, без новых элементов диалога. **AC6. Гейты реализации зелёные.** См. таблицу выше — все обязательные и применимые по diff/AC гейты прогнаны и зелёные; полные golden/smoke/performance не требуются и оставлены предрелизным гейтом, как и разрешает ТЗ §10 явным текстом «Golden не требуется для этого невизуального изменения». ## Одно число — один источник Диф не добавляет и не показывает пользователю никакую числовую величину (площадь, толщину, подпись, подсветку) — только логическую доступность привязки в списке и её runtime-приоритет. Правило неприменимо к этому диффу. ## Что проверено и корректно - Единая точка исключения (`isRemovedPlanEntity`) вместо рассеянных патчей — снижает риск того, что новый runtime-потребитель забудет учесть live-override. - Layout-идентичность X независима от D (`markerIdForBinding('entity:X')` → `lg_X`, не совпадает с `dev.id` D) — подтверждено чтением `src/logic.ts` и тестом на `childGetsFreshLayoutPosition`/юнит-ассерт `lg_sensor.voice_level`. - Перф: новый `liveEntities` Set собирается в уже существующем единственном проходе по `markers[]` внутри `removedPlanBindings()`, проверка — O(1) по Set; никакого нового прохода по registry на render не добавлено. - Процессные формальности: трейлеры `Issue`/`User-Visible` на месте во всех 5 коммитах диапазона; `User-Visible: yes` только на коммите с продуктовым изменением, и там же оба changelog; ветка/issue соответствуют; `process-gate.mjs` офлайн-проверка пройдена; CI `Validate` зелёный на точном финальном SHA; Medium-находка предыдущего этапа корректно заведена отдельным issue (#269), не тронута в этой ветке. ## Вердикт Зелёный. High: 0, Medium: 0 (в скоупе — 0; вне скоупа — уже закрыто #269 на этапе ревью ТЗ, здесь новых находок вне скоупа нет).