mirror of
https://github.com/Matysh/houseplan-card
synced 2026-09-29 03:09:36 +00:00
Ревью #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
199 lines
16 KiB
Markdown
199 lines
16 KiB
Markdown
# Код-ревью #226 — r1
|
||
|
||
- Issue: [#226](https://github.com/Matysh/houseplan-card/issues/226)
|
||
- ТЗ: [`docs/specs/226-entity-parent-dedup.md`](../../specs/226-entity-parent-dedup.md)
|
||
(зелёное ревью r2: [`SPEC-REVIEW-226-r2.md`](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.ts` diff целиком: `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.mjs` diff целиком —
|
||
сопоставлено с матрицей §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, explicit `device: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/**` изменился), все 10 `imageSha256` идентичны 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-маркеров на скриншотах
|
||
документации, а там все 10 `imageSha256` не изменились. Риск, который
|
||
`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`.
|