13 KiB
CODE-REVIEW-262-r1
- Issue: #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 (проверено:
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.idD) — подтверждено чтениемsrc/logic.tsи тестом наchildGetsFreshLayoutPosition/юнит-ассертlg_sensor.voice_level. - Перф: новый
liveEntitiesSet собирается в уже существующем единственном проходе поmarkers[]внутриremovedPlanBindings(), проверка — O(1) по Set; никакого нового прохода по registry на render не добавлено. - Процессные формальности: трейлеры
Issue/User-Visibleна месте во всех 5 коммитах диапазона;User-Visible: yesтолько на коммите с продуктовым изменением, и там же оба changelog; ветка/issue соответствуют;process-gate.mjsофлайн-проверка пройдена; CIValidateзелёный на точном финальном SHA; Medium-находка предыдущего этапа корректно заведена отдельным issue (#269), не тронута в этой ветке.
Вердикт
Зелёный. High: 0, Medium: 0 (в скоупе — 0; вне скоупа — уже закрыто #269 на этапе ревью ТЗ, здесь новых находок вне скоупа нет).