docs: review document for #262

Issue: #262
User-Visible: no
This commit is contained in:
claude[bot]
2026-08-23 13:54:22 +00:00
parent 5fa2dc2893
commit 3e3fb9ceaa
+160
View File
@@ -0,0 +1,160 @@
# 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 на
этапе ревью ТЗ, здесь новых находок вне скоупа нет).