Issue: #117 User-Visible: no
21 KiB
CODE-REVIEW-117-r2
- Issue: https://github.com/Matysh/houseplan-card/issues/117
- ТЗ:
docs/specs/117-registryless-opening-entity.md, зелёноеdocs/reviews/SPEC-REVIEW-117-r1.md(High 0, Medium 0) - Предыдущий цикл:
docs/reviews/CODE-REVIEW-117-r1.md— зелёный, High 0, Medium 0. Слияние не удалось (конфликт сdev), задача вернулась вS6-in-progressне по правкам кода, а для ребейза (комментарий владельца от 2026-08-18). Ветка ребейзнута на актуальныйorigin/devи опубликована--force-with-leaseнаc65cbcc96c644a2bfca3dbd6c62482dc16c092c3. - Диапазон:
git log --oneline origin/dev..HEAD— четыре коммита:563a850docs: specify registryless opening entities(Issue: #117,User-Visible: no)9baf533docs: review document for #117(Issue: #117,User-Visible: no)01fe48dfix: support registryless opening entities(Issue: #117,User-Visible: yes)c65cbccdocs: review document for #117(Issue: #117,User-Visible: no)
- Роль: ревьюер кода (не автор), свежая сессия без контекста реализации,
этап
S7-code-review, цикл r2/4 (лимит считается по этапу — вердикт ТЗ бюджет код-ревью не расходует).
Скоуп ревью
git diff origin/dev...HEAD --stat — 18 файлов:
| Класс | Файлы |
|---|---|
| A (продукт) | src/ha-binding-status.ts |
| B (гейты/инструменты) | demo/smoke_registryless_opening.mjs (новый), scripts/mutation-gate.mjs, test/ha-binding-status.test.mjs, test/render-device-snapshot.test.mjs |
| C (документация) | docs/ARCHITECTURE.md, docs/CHANGELOG.md, docs/CHANGELOG.ru.md, docs/TESTING.md, docs/USER-GUIDE.ru.md, docs/images/screenshots.json, docs/specs/117-registryless-opening-entity.md, docs/specs/README.md, docs/reviews/SPEC-REVIEW-117-r1.md, docs/reviews/CODE-REVIEW-117-r1.md |
| D (сгенерированное) | dist/houseplan-card.js, custom_components/houseplan/frontend/houseplan-card.js, demo/srv/assets/houseplan-card.js |
Что изменилось между r1 и r2: сам продуктовый диф не изменился ни на
строку — git show 01fe48d -- src/ha-binding-status.ts даёт байт-в-байт тот же
патч, что был одобрен в r1 (единственная точка: renderOpeningEntityAvailable
в src/ha-binding-status.ts:533-546 больше не требует одновременно
projectedHass.entities[entityId] и projectedHass.states[entityId], только
наличие state в замороженной проекции). Ребейз добавил только слияние трёх
generated-бандлов с ушедшим вперёд dev (конфликт разрешён пересборкой) и
docs/images/screenshots.json (изменился только sourceFingerprint/
sourceSha256 из-за нового содержимого бандла; все imageSha256 — то есть сам
пиксельный результат существующих golden-сцен — не изменились). Это не
формальность: я не полагался на констатацию «код тот же», а сверил патч
построчно и независимо пересобрал бандл, см. ниже.
Как проверялось
Я не ассистент автора — свежая сессия, ни один гейт не принят «со слов».
Все команды ниже прогнаны лично на этом дереве (origin/issue/117-registryless-opening
@ c65cbcc).
| Гейт | Команда | Результат |
|---|---|---|
| Typecheck | npx tsc --noEmit |
зелёный, без вывода |
| Unit | npm test |
870/870 pass (совпадает с заявленным автором после ребейза; в r1 было 804/804 — рост за счёт коммитов dev, вошедших при ребейзе) |
| Build + сверка бандлов | npm run build, затем sha256sum трёх копий |
зелёный; все три — 72f660c0c574574ab07d8d7dd96262c928f333dbe5747e8091c48fbb6f0094ca, совпадает с заявленным автором хешем и с уже закоммиченными файлами (git status --short после сборки — пусто) |
| Целевой smoke (AC1/2/3/7/8/9) | node demo/smoke_registryless_opening.mjs |
OK, все 13 подпроверок true |
| Регрессия opening-биндинга | node demo/smoke_opening_binding.mjs |
OK, все 15 подпроверок true |
| Регрессия lock action | node demo/smoke_lock_action.mjs |
OK, все 8 подпроверок true |
| Регрессия lock invariant (SCOPE.md) | node demo/smoke_lock_invariant.mjs |
OK, все 7 подпроверок true |
| Mutation-guard | node scripts/mutation-gate.mjs --id=registryless-opening-requires-registry-row |
поймано 1 из 1 — возврат .entities[entityId] красит smoke, чистый прогон зелёный |
Process-gate (офлайн + --issues) |
node scripts/process-gate.mjs --range origin/dev..HEAD --target-ref refs/heads/issue/117-registryless-opening --issues |
коммитов 4, гейт пройден, предупреждений 0 |
Что не прогонял и почему (соразмерность гейтов, PROCESS.md §8, диф не менялся относительно уже принятого r1):
- Полный набор из 127 browser-смоков — diff по-прежнему задевает ровно одну логическую точку и один класс потребителей (opening contact/lock). Целевой smoke плюс три регрессионных (opening-биндинг, lock action, lock invariant) покрывают contract, security-инвариант и frame-atomicity — этого достаточно для поверхности изменения; расширение до полного набора не добавило бы сигнала сверх r1.
npm run golden:verify— не прогонял. Прочитал diffsrc/space-render.tsи геометрических модулей вorigin/dev...HEAD— их там нет; единственная правка убирает один операнд&&, который для уже зарегистрированных сущностей (entities[eid]иstates[eid]присутствуют или отсутствуют синхронно) не меняет результат вовсе.docs/images/screenshots.jsonв этом же коммите подтверждает то же самое замером:imageSha256всех 10 сцен не изменился, изменился толькоsourceFingerprint(хеш нового бандла). Ни один golden-сценарий не использует YAML-only fixture без registry row.python -m pytest tests_backend -q— не прогонял, диф не касаетсяcustom_components/houseplan/**/*.py(таких файлов в изменениях нет).- Performance-профили — не прогонял. ТЗ §14 явно заявляет «нет», правка не добавляет новый проход по кадру или структуру данных.
- Реальную установку Home Assistant с живой YAML-сущностью без
unique_id— вне возможностей код-ревью; демо-стенд с фейковымhass/registry — канонический суррогат поAGENTS.md, и я прогнал его лично. - Ребейз/историю ветки как таковую (корректность merge-base, что именно
было в конфликте) — не переисполнял
git rebase; вместо этого сверил итог: продуктовый патч побайтово идентичен r1, три бандла воспроизводимо собираются из текущего дерева с тем же хешем, что заявил автор,npm testзелёный на полном (после ребейза выросшем) наборе. Это доказывает результат ребейза, а не процесс его выполнения — для код-ревью этого достаточно. - Локальный
pre-push, обойдённый автором при публикации ребейзнутой ветки — не входит в скоуп код-ревью #117: это отдельный дефект инструмента (диапазонpre-pushпосле--force-with-leaseневерно берёт старый remote SHA), заведён владельцем отдельно как #190 (process,bug,P2,S1-new— провереноgh issue view 190, issue существует с ровно этими метками). Страховка CI (process-gatejob вvalidate.yml) не обходится этим действием, и я независимо прогнал тот же скрипт офлайн — гейт чист.
Находки
Находок уровня High и Medium нет.
Low не завожу. Диф идентичен уже одобренному в r1, повторное построчное чтение
не выявило ничего нового; единственное новое обстоятельство ребейза
(docs/images/screenshots.json) — ожидаемое следствие смены хеша бандла, а не
дефект.
Что проверено и корректно
Ревью выполнено заново, не как штамп поверх r1: каждый AC перепроверен чтением текущего кода на этом SHA и/или повторным исполнением, без опоры на текст предыдущего документа как на источник истины.
- AC1 (registry-less live contact меняет presentation). Прочитан
_openingAmt()(src/houseplan-card.ts:17101-17107): amount берётся изthis._renderPlanHass.states[o.contact]только когда_renderOpeningEntityAvailable(o.contact)истинно. Доказано smoke:closedContactControlsPresentation(closed) иstateTickSwapsOneAtomicFrame(open) наbinary_sensor.hp117_yaml_contact, для которой smoke явно проверяетnoRegistryRowsExist(нет строки вcard.hass.entities). - AC2 (registry-less live lock badge).
_renderOpeningLocks()(src/houseplan-card.ts:17221-17224) фильтрует по тому же_renderOpeningEntityAvailable(o.lock). Доказано smoke:yamlLockBadgeRendersLocked, затем.oplock.unlockedпосле смены состояния,unknownKeepsExistingTypeSemantics—unknownне становится ниlocked, ниunlocked, значок.oplock.unknown. - AC3 (frame — только immutable projection, не raw hass). Прочитано:
_renderOpeningEntityAvailable(src/houseplan-card.ts:17129-17131) — однострочная делегация вrenderOpeningEntityAvailable(this._renderPlanHass, eid),this.hassне упоминается нигде в теле. Source-contract тестtest/render-device-snapshot.test.mjsпроверяет это регексом на тело именно этого метода (assert.doesNotMatch(openingRenderAvailability, /this\.hass\b/)) и черезmethodBody. Mutation-гейт подтверждает, что тест красится при регрессии (возврат.entities[entityId]), а не только выглядит проверкой. - AC4 (active registry entity — прежнее поведение). Логическое обоснование
подтверждено чтением
activeRegistryHass()(src/ha-binding-status.ts:323-354): для зарегистрированной активной сущностиentities[eid]иstates[eid]присутствуют или отсутствуют синхронно — удаление одного операнда&&не меняет результат. Исполнением подтвержденоsmoke_opening_binding.mjs(exactOpeningReferencesStayActiveи вся регрессионная матрица, 15/15). - AC5 (disabled/orphan/missing остаются unavailable). Прочитан
activeRegistryHass()построчно: явныйdisabled_byна сущности или родительском устройстве, а также authoritative-orphan (device_idуказывает на отсутствующее устройство) — все три ветки удаляютstateиз проекции до её передачи в render helper, поэтомуrenderOpeningEntityAvailableчестно не может их оживить. Доказано unit (test/ha-binding-status.test.mjs, кейсыlock.disabled_entity,lock.disabled_parent,lock.orphan,lock.missing— всеfalse) и smoke (explicitDisabledRowsRemoveStaleStates, с ожиданием debounce-паузы220msперед проверкой удаления state). - AC6 (limited-registry live exact entity работает). Доказано unit —
limitedFrameсauthoritative: falseи без registry rows,renderOpeningEntityAvailable(limitedFrame, 'binary_sensor.limited_yaml')→true. - AC7 (marker tombstone не блокирует opening reference). Доказано unit
(
markerTombstoneIsNotAnInput— projection сremoved: trueмаркером той же сущности всё равно даётtrue) и smoke (markerTombstonesDoNotBlockOpening, который дополнительно контрастирует с!card._planEntityAvailable(...), то есть общий marker-путь по-прежнему блокируется, а opening-путь — нет; узкий скоуп из ТЗ §4 не создаёт разрыва). - AC8 (lock security/unlock confirmation не меняются). Прочитан
_lockAction()(src/houseplan-card.ts:17260-17273) — вне дифа, по-прежнему проверяет_openingEntityAvailable()(live path) передcallService,confirm()только дляunlock. Значок.oplockпо клику лишь открывает info-карточку (this._openingInfo = o), не вызывает сервис — прочитано наsrc/houseplan-card.ts:17246-17250. Подтверждено исполнением:smoke_lock_action.mjs,smoke_lock_invariant.mjsзелёные (SCOPE.md-инвариант «The lock invariant, stated precisely» не ослаблен), плюс новый smoke явно проверяетplanOpeningAndBadgeNeverActuate(тап по проёму и badge не меняет число вызововcallService) иexplicitInfoActionStillWorks(только явное действие в открытой info-карточке вызываетlock.unlock). - AC9 (state update не пересобирает geometry/config). Доказано smoke:
stateTickDoesNotRebuildGeometryOrConfigсравнивает_physicalBodiesCache,_cfgEpochи сериализованный_serverCfgдо/после тика состояния. - AC10 (existing opening golden/interactions без регресса). Golden baseline
в дифе не тронут;
docs/images/screenshots.jsonпоказывает, что все 10imageSha256не изменились — то есть уже принятые пиксельные сцены идентичны и после ребейза. Поведенческая регрессия подтверждена отдельно исполнениемsmoke_opening_binding.mjs(зелёный). - Трейлеры и changelog. Все четыре коммита несут терминальные
Issue: #117иUser-Visible: yes|no(провереноgit show -s --formatпо каждому). Коммит сUser-Visible: yes(01fe48d) правитdocs/CHANGELOG.mdиdocs/CHANGELOG.ru.mdв себе же (git show 01fe48d --stat), обе записи под одним и тем же заголовкомv1.65.0-beta.2, без дублирования с уже существующими записями. - Соответствие ТЗ и SCOPE.md. Диф не расширяет скоуп (§4 ТЗ дословно
соблюдён: picker, миграция, marker lifecycle, generic registry-less для
прочих marker-биндингов, geometry не тронуты). Задача лежит внутри уже
закрытых J1/J3/J6 (
docs/SCOPE.md), lock-инвариант не ослаблен. - Целостность ребейза. Продуктовый патч (
src/ha-binding-status.ts) побайтово идентичен версии, одобренной в r1 (свереноgit showdiff-текста). Три копии бандла воспроизводимо пересобираются из текущего дерева с тем же SHA-256, что заявил автор в хендоффе.git status --shortпосле сборки — пусто: закоммиченные бандлы уже соответствуют пересборке, конфликт разрешён корректно, а не «на глаз».
Чего не проверял
- Полный набор из 127 browser-smoke — предрелизный гейт, не гейт ревью; обоснование сужения дано выше и не изменилось относительно r1.
npm run golden:verify— обоснование дано выше (imageSha256неизменны, геометрические/стилевые модули вне дифа).python -m pytest tests_backend -q— Python не тронут этим дифом.- Performance-профили — ТЗ §14 явно исключает влияние, изменение не меняет проход по кадру.
- Реальную установку Home Assistant с YAML-сущностью без
unique_id— вне возможностей ревью; демо-стенд — канонический суррогат, прогнан лично. - Сам процесс
git rebase(порядок разрешения конфликтов, промежуточные состояния) — не переисполнялся; проверен итог (см. «Целостность ребейза» выше), что для код-ревью достаточно. - Дефект
pre-pushпри force-push после rebase — вне скоупа этого issue, отслеживается отдельно в #190; подтверждено, что issue заведён и что CI backstop (process-gate) при этом не обойден (перепрогнан лично, чист).
Вердикт
Зелёный. High: 0, Medium: 0. Ребейз не изменил продуктовый код ни на строку — проверено побайтовым сравнением диффа, а не заявлением автора; три копии бандла воспроизводимо пересобираются с тем же хешем; полный (выросший после ребейза) unit-набор 870/870, typecheck и целевые/регрессионные smoke зелёные, mutation-guard подтверждает, что покрытие умеет падать. Каждый AC1–AC10 либо доказан автотестом, который я лично прогнал, либо разобран чтением текущего кода на этом SHA с явной пометкой «проверено чтением, не исполнением». Трейлеры, changelog (RU+EN) и process-gate в порядке. Задача готова к слиянию.