Ревью #682 r1, Medium: перенос добавляет документу уровень вложенности (`docs/reviews/X.md` → `legacy/reviews/<тег>/X.md`, `docs/specs/` → `legacy/specs/`), а относительные ссылки внутри перенесённых документов и в соседях, ссылавшихся на них, никто не пересчитывал — на `97d19268` 53 битые ссылки в 46 файлах (заявление «все 26 резолвятся» в `7feb6177` было верно только до переноса документов ревью). Гейты архив не смотрят. `reviews-archive.mjs`: `repairLinks` пересчитывает ссылку, если она не резолвится от нового места, а цель находится от нового или старого места через карту переносов; битая и до переноса ссылка не трогается. `--apply` делает это само, `--repair-links=<rev>` — для всех переименований `<rev>..HEAD`, `--check-links` печатает битые. Этим коммитом `--repair-links=origin/dev` переписал ровно 53 ссылки в 46 файлах; остались две прежние «...»-заглушки в CODE-REVIEW-448-r2 (битые и на dev). Тесты: перенесённый документ, сосед со ссылкой в архив, ТЗ со ссылкой на позже перенесённое ревью, битая-до-переноса не трогается, в `legacy/` битых нет; мутант `reviews-archive-links-from-new-place-only`. PROCESS §2.10 и DEVELOPMENT › Release называют переписывание и `--check-links`. Issue: #682 User-Visible: no Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018qZfe7YS4rqEMKoVeS3GKd
16 KiB
Код-ревью #226 — r1
- Issue: #226
- ТЗ:
docs/specs/226-entity-parent-dedup.md(зелёное ревью r2:SPEC-REVIEW-226-r2.md) - Ветка:
issue/226-entity-parent-dedup, коммит реализацииf151e70 - Материал:
git diff origin/dev...HEAD,git log --oneline origin/dev..HEAD - Вердикт: зелёный · цикл r1/4 · High: 0 · Medium: 0
Скоуп разбора
Первый цикл код-ревью — разбор полный. Диапазон коммитов от dev: два
docs-коммита спец-ревью (уже приняты на этапе spec), один implementation
commit f151e70. Продуктовый код меняется только в src/devices.ts
(buildDevices, seedHiddenBindings + два новых internal helper).
Сопутствующие изменения: test/devices.test.mjs, demo/smoke_entity_parent_dedup.mjs,
scripts/mutation-gate.mjs, docs/FILTERING.md, docs/USER-GUIDE{.ru,}.md,
docs/TESTING.md, оба changelog, docs/specs/README.md, три копии bundle.
Как проверялось
Прочитано построчно
src/devices.tsdiff целиком:entityMarkerOwnership(),residualAutoDeviceEntities(), их встраивание вbuildDevices()(авто-цикл устройств, строки ~1092–1144) и вseedHiddenBindings()(~1004–1030), а также неизменённые ветки — явныйdevice:D(~1180–1210) и явныйentity:X(~1211–1245), чтобы подтвердить заявленную асимметрию.src/ha-binding-status.ts:activeRegistryHass,fullRegistryHass,isRegistryEntryEnabled,HaBindingStatus— чтобы проверить, что реконструкцияitemBindingStatusдля частичного остатка ({kind:'active', enabledEntityIds: entIds, allEntityIds: entIds}) не роняет поля, которых нет в вариантеactive, и чтоhass.entities[eid].hidden— тот же нормализованный frontend-флаг, что уже используется вvisibleFirst()(devices.ts:147) — не новое допущение этого PR.- Потребители
allEntities/entitiesвнеdevices.ts(device-presentation.ts:247,device-toggle.ts:384,723) — подтверждено, что частичный остаток сознательно не протекает черезallEntities(§6 ТЗ), а не через недосмотр: если единственный cover в остатке скрыт HA,allEntitiesкорректно не содержит его, и это ровно граница #94, которую ТЗ объявляет ожидаемой. test/devices.test.mjsиtest/mutation-gate.test.mjsdiff целиком — сопоставлено с матрицей §12 ТЗ (таблица ниже).docs/FILTERING.md,docs/USER-GUIDE.md,docs/USER-GUIDE.ru.md,docs/TESTING.md, обаCHANGELOG— сверено с §16 ТЗ и с формулировками, принятыми на ревью ТЗ (§3.3/§8), терминология не изобретена.
Выполнено (не только прочитано)
npx tsc --noEmit → passed, без вывода
npm test → 971 passed / 0 failed / 0 skipped
npm run build → passed
sha256sum dist/… custom_components/…/frontend/… demo/srv/assets/…
→ одинаковый хеш на всех трёх копиях,
совпадает с хешем из хендоффа автора
(795513ec6d15…)
node scripts/mutation-gate.mjs --id=entity-marker-kept-in-parent-device
→ чистый прогон ok, мутант "покраснел, как обязан"
node scripts/mutation-gate.mjs --id=entity-marker-parent-seeded
→ чистый прогон ok, мутант "покраснел, как обязан"
node demo/smoke_entity_parent_dedup.mjs → OK, все 8 planFacts/previewFacts/staticFacts true
node demo/smoke_cover_not_primary.mjs → OK, 36/36 фактов true (регресс #94 не пойман)
node demo/smoke_cover_tap.mjs → OK, 31/31 фактов true
node scripts/check-docs.mjs --external → passed (7 файлов, 10 внешних ссылок)
npm run inventory → 971 unit / 144 pure backend / 114 HA-harness / 156 smokes
(сверено с записью автора)
Дисциплина «тест должен уметь падать» применена к обоим мутационным guard: у каждого проверен и чистый прогон (ok), и то, что патч именно ломает целевой тест, а не проходит мимо.
Находки
Нет находок ни High, ни Medium. Один Low, снят с записью ниже.
Low — тест-матрица §12.7 не покрывает первую половину кейса напрямую.
ТЗ (кейс 7, AC4) требует: «явная HA-hidden entity работает как exact marker».
Тест hidden-only curtain residual disappears… доказывает только вторую
половину (hidden sibling не удерживает остаток). Прямого юнита «маркер
entity:X, где сама X имеет reg.hidden: true, и это не глушит explicit-ветку»
нет.
Снимаю без правки: ветка kind === 'entity' в buildDevices (строки
~1211–1245) не читает hidden/reg.hidden ни в этом коммите, ни до него —
diff её не касается вовсе. Поведение «explicit entity marker показывается
независимо от HA-hidden» не новое и не зависит от entityMarkerOwnership;
риск регрессии от этого PR отсутствует, потому что PR не добавляет туда ни
одной строки. Это пробел в тест-документации ТЗ (заявлена AC-покрытием кейса,
которого нет буквально), не дефект кода. Не блокирует — верно чтением, а не
исполнением, что здесь и достаточно.
Что проверено и корректно
- AC1 (нет полного дубля).
residualAutoDeviceEntitiesвычитаетplacedEntityIdsизentsBy[dev.id]; при пустом остаткеif (residual.partial && !residual.entityIds.length) continue;— устройство не попадает в вывод ни вbuildDevices, ни вseedHiddenBindings. Юнит-кейсы 1/3 зелёные, мутантentity-marker-kept-in-parent-deviceподтверждён падающим при откате правила. - AC2 (частичный остаток). Тест «partial auto parent contains only visible
unclaimed siblings» проверяет не только
entities, но иallEntities,bindingStatus,primaryиresolvedLightSources— заявленная claimed light не протекает повторно.itemBindingStatus— валидныйactive-вариантHaBindingStatus(проверено типом вha-binding-status.ts:14), typecheck зелёный. - AC3 (явная асимметрия). Тест «explicit device and child entity markers
coexist intentionally»:
device:Dсохраняет полный состав через неизменённую ветку (~1187,bindingStatus.enabledEntityIds), не пересекается сresidualAutoDeviceEntities. Пре-существующий тест «entity tombstone does not strip that entity from a live parent device» (строка 451, не в дельте) прогнан в общемnpm testи зелёный —entityMarkerOwnershipкорректно пропускаетmarker.removed === true. - AC4 (hidden-контракты + #94).
residualAutoDeviceEntitiesвозвращает{partial:false, entityIds:[...entityIds]}, когдаownership.byDeviceне содержит записи для устройства — нетронутый auto/device:Dне фильтрует hidden вообще, что и требует §8 ТЗ. Подтверждено смокамиsmoke_cover_not_primary.mjs/smoke_cover_tap.mjs(регресс #94 не воспроизведён) и юнитом «hidden-only curtain residual disappears but explicit device stays cover-first» — untouched-ветка даётprimary: 'cover.curtain', split-ветка убирает auto-marker, explicitdevice:Dвосстанавливает штору. Marker-hidden (case 5) и HA-disabled (case 9) отдельно покрыты юнитами и дают верныйghost/hiddenстатус. - AC5 (standalone/группы). Ветка light-groups (
groups,claimed.has('entity:'+g.eid)) не тронута диффом; helper безdevice_idпропускается вentityMarkerOwnershipчерезif (!deviceId) continue;. - AC6 (seeder parity).
seedHiddenBindingsиспользует те жеentityMarkerOwnership/residualAutoDeviceEntities, не дублирующую реализацию — единственный источник правила, как требует §6 ТЗ. Тест «entity ownership uses the same visible residual as buildDevices» и мутантentity-marker-parent-seededподтверждают. - AC7 (все renderers).
demo/smoke_entity_parent_dedup.mjsпроверяет полный View/kiosk-DOM (390×760, touch-профиль), клик по exact-marker (light.ceiling, точныйturn_off, конфиг не перезаписывается), Device editor preview (hp-device-preview— ровно одна.dev-морда) иhouseplan-space-card(статическая карта видит только exact-сущность). - AC8 (динамический registry). Юнит «registry hidden sibling changes
rebuild the residual without config writes» — два снапшота (
hidden/visible) дают разный список без переписывания маркера (assert.deepEqual(marker, …)на неизменный объект). - AC9 (совместимость). Diff не трогает
ServerConfig, backend, i18n-ключи, wire protocol.npx tsc --noEmitзелёный,npm run buildзелёный. - AC10 (release-артефакты). Оба changelog в том же коммите
f151e70(Issue: #226,User-Visible: yes),docs/FILTERING.md/USER-GUIDE{.ru,}.md/TESTING.mdописывают ownership тем же языком, что принят на ревью ТЗ.docs/images/screenshots.json:sourceFingerprintобновился (ожидаемо —src/**изменился), все 10imageSha256идентичны dev — визуальной дельты в захваченных сценариях нет. Три копии bundle синхронны по SHA-256. - Производительность (§17 ТЗ).
entityMarkerOwnership— один проход поmarkers(O(markers)),residualAutoDeviceEntities—O(1)lookup на сущность черезSet/Map, никакого вложенного поиска markers внутри device/entity циклов. Проверено чтением, не исполнением (нет отдельного perf-теста в AC этой задачи). - Мутационные guards. Оба id из §14 ТЗ присутствуют в
scripts/mutation-gate.mjsи подтверждены индивидуальным прогоном (см. выше).test/mutation-gate.test.mjsне менялся, но перебираетMUTANTSобобщённо — новые id уже под его структурными проверками (patch anchors exactly once,guard file exists,explains itself). - Трейлеры.
f151e70:Issue: #226,User-Visible: yes— терминальные, оба changelog в том же коммите.
Чего не проверял и почему
npm run golden:verifyне запускался. Golden-фикстуры (demo/golden/matrix.mjs,harness.mjs) — про геометрию стен/комнат/glow, ни один сценарий не строит устройство с несколькими сущностями и явным entity-marker на части из них (проверено grep по фикстурам). Единственный канал, где эта задача могла задеть видимый пиксель, — количество/состав device-маркеров на скриншотах документации, а там все 10imageSha256не изменились. Риск, которыйgolden:verifyмог бы поймать сверх уже пройденных смоков, оцениваю как пренебрежимо малый для объёма этой правки; полный golden — гейт предбета (PROCESS.md §8), не код-ревью.npm run golden:capture/принятие baseline не запускалось — не требуется: геометрия marker не меняется (ТЗ §15), новых сценариев нет.python -m pytest tests_backendне запускался — диф не касаетсяcustom_components/houseplan/**/*.pyни одним файлом (только сгенерированный frontend-бандл подcustom_components/houseplan/frontend/, класс D).- Полный набор из 156 браузерных смоков не прогонялся. Прогнаны три:
новый
smoke_entity_parent_dedup.mjs(назван в AC7/матрице §12.15) и два, которые делят с диффом общий resolver #94 (smoke_cover_not_primary.mjs,smoke_cover_tap.mjs). Остальные смоки покрывают геометрию/openings/sun/ touch-жесты — поверхности, которых этот diff не трогает. - Performance-профиль на синтетическом большом registry не запускался: не назван в AC этой задачи, а линейность подтверждена чтением кода (раздел выше).
- Ручное тестирование в браузере не проводилось — по процессу код-ревью этого цикла работает по диффу и автоматическим доказательствам, не по живому UI.
Итог
Все 10 AC доказаны автотестом или прочитанным кодом с явной пометкой способа
проверки; оба мутационных guard подтверждены индивидуальным прогоном, включая
проверку, что они действительно способны покраснеть. Единственная находка —
Low, документационный пробел тест-матрицы без функционального риска, снят с
запиской выше. Готово к статусу «Принято»/S8-merged.