Files
houseplan-card/docs/reviews/CODE-REVIEW-262-r1.md
T
2026-08-23 13:54:22 +00:00

161 lines
13 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 на
этапе ревью ТЗ, здесь новых находок вне скоупа нет).