14 KiB
Код-ревью issue #174 — r1
- Issue: https://github.com/Matysh/houseplan-card/issues/174
- ТЗ:
docs/specs/174-linked-virtual-light-controller.md(ревью:docs/reviews/SPEC-REVIEW-174-r1.md, вердикт зелёный) - Диапазон:
origin/dev..HEAD, коммиты4a89582(ТЗ) ·da10b3d(spec-review doc) ·2f9cced(реализация,User-Visible: yes) ·ea2633a(docs-only, screenshot fingerprint) - Ревьюер: Claude, свежая сессия, без контекста реализации
Скоуп проверки
Правка меняет authority для manual-eligible виртуального источника света
(точная тройка #107: binding=virtual + is_light=true + tap_action=toggle)
при наличии входящей связи controls: [marker:<id>] от реального контроллера:
вместо безусловного перекрытия ручным state теперь реальный driver HA — единый
источник истины, а клик по любому из двух marker переключает реальное
устройство. Несвязанный источник сохраняет поведение #107 без изменений.
Затронуты: src/devices.ts (canonical light graph, resolvedLightSources),
src/device-toggle.ts (resolveToggleIntent, resolveControls), их тесты,
demo/smoke_linked_virtual_light.mjs, документация и оба changelog.
src/houseplan-card.ts не тронут — confirmation/re-resolve уже был общим
механизмом через sameToggleOperationTargets, что подтверждено чтением.
Как проверялось
| Гейт | Команда | Результат |
|---|---|---|
| Typecheck | npx tsc --noEmit |
зелёный, без вывода |
| Unit | npm test |
838/838 pass, 0 fail (совпадает с хендоффом) |
| Build | npm run build |
зелёный, dist/houseplan-card.js создан за 10.9s |
| Три копии бандла | sha256sum dist/houseplan-card.js custom_components/houseplan/frontend/houseplan-card.js demo/srv/assets/houseplan-card.js |
все три d79008ee8d74…502e8 — совпадают побайтово и совпадают с SHA-256 из хендоффа |
| Целевой smoke (AC9–AC11) | node demo/smoke_linked_virtual_light.mjs |
OK, все 31 проверка true (клик по лампе/контроллеру, HA-tick, Glow/fill/label, touch tap/long-press/pan/pinch/pointercancel, unlink→manual restore) |
git status после build |
— | пусто, сгенерированные копии не разошлись с закоммиченными |
Дисциплина «тест умеет падать» — не поверье, а проверено мутацией:
- В
src/devices.tsвременно убрано условие!control &&перед перекрытием ручным state (возвращена версия бага). Пересборкаtest-build→node --test test/devices.test.mjs→ красные ровно AC1-тесты:issue 174: linked manual virtual source follows controller state…иissue 174: one cached reverse graph owns linked state…. Файл восстановлен, тесты снова зелёные (78/78 в этом файле). - В
src/device-toggle.tsвременно убрана веткаif (incoming) return resolveIncomingControllers(...)вresolveToggleIntent. Пересборка →node --test test/device-toggle.test.mjs→ красные ровно 3 теста AC3/AC4/AC5 («redirects its toggle to the real controller driver», «source unions all drivers…», «keeps partial availability…»). Файл восстановлен, 35/35 зелёные. - После обоих восстановлений — полный
npm run build, сверка трёх копий бандла иgit statusпусты: рабочее дерево вернулось к committed-состоянию.
Не прогонялось, и почему:
- Полный набор
demo/smoke_*.mjs(127 файлов). Diff ограничен резолвером canonical light graph и explicit Toggle; остальные поверхности (проёмы, стены, sun, изометрия и т.д.) не затронуты ни одним изменённым файлом. Достаточно целевого smoke, названного в AC9–AC11. npm run golden:verify. Проверено чтением фикстур: ни один сценарийdemo/goldenне использует комбинациюbinding: virtual+is_light+ входящую связьcontrols: [marker:*](grepпо конфигу голден-сцен не дал совпадений наvirtual/is_light). Диапазон изменения не может задеть существующие эталоны, а новый не нужен — визуал on/off не меняется, меняется только источник его вычисления.python -m pytest tests_backend -q. Diff не касается ни одного файлаcustom_components/houseplan/**/*.py;git diff --statподтверждает.- Performance-профили. В ТЗ (§15) заявлено «нового численного budget нет»;
reverse-граф переиспользует уже существующий кэш
lightGraphOf(LIGHT_GRAPH_CACHE, инвалидация по структурному fingerprint конфигурации, не по HA state/pointer-событию) — проверено чтением, не исполнением. Профиль не назван в AC, оставлен пре-релизному гейту.
AC — разбор
| AC | Доказательство | Вердикт |
|---|---|---|
| AC1 | unit, мутация красит | Подтверждено |
| AC2 | unit (resolvedLightSources с изменённым HA-state и неизменным virtualLights.rev, а также с изменённым rev без смены driver — оба случая в тесте «one cached reverse graph…») |
Подтверждено |
| AC3 | unit, мутация красит; on.targets[0].via === 'control-marker-driver', toggleOperation(on).kind === 'ha-service' |
Подтверждено |
| AC4 | unit («source unions all drivers…»): switch.a+switch.b, детерминированный порядок, any-on → turn_off объединения |
Подтверждено |
| AC5 | unit («keeps partial availability…»): missing/unavailable/ha-disabled пропущены и объяснены skippedTargets; zero-driver dormant → command: null, noneReason: 'configured-targets-missing', toggleOperation(none) === null (без virtual-light fallback) |
Подтверждено |
| AC6 | unit («source unions all drivers…», вторая половина): клик по relayA/relayB даёт только собственный target каждого, тем же driverEids, что участвуют в linked-состоянии источника |
Подтверждено |
| AC7 | unit (переименованный тест #107→#174, ветка unlinked) + отдельный regression-тест #107 без incoming link (test/device-toggle.test.mjs:73, controls: ['light.saved'] — это исходящая ссылка устройства, не входящая) остался зелёным без изменений |
Подтверждено |
| AC8 | ревью кода: sameToggleOperationTargets меняет идентичность target set при переходе linked↔manual (unit-тест unlinked в device-toggle.test.mjs: sameToggleOperationTargets(on, unlinked) === false); confirmation в houseplan-card.ts уже переоценивает intent через этот примитив на каждый confirm — проверено чтением src/houseplan-card.ts:4387-4396, не исполнением через реальный UI-диалог |
Подтверждено (частично чтением) |
| AC9 | smoke smoke_linked_virtual_light.mjs: sourceCallsRelay, controllerCallsRelay, offTickUpdatesAll, controllerTickUpdatesAll, noOperationalToggleWhileLinked |
Подтверждено |
| AC10 | тот же smoke: touchTapCallsOnce, longPressOpenedInfo/longPressNoService, pointerCancelNoService, panNoService, pinchNoService |
Подтверждено |
| AC11 | тот же smoke: externalStateUpdateWorks (HA-tick без клика), unlinkRestoresManualOff, manualToggleRestored |
Подтверждено |
| AC12 | ревью кода + unit: единственный incomingLightControls()/lightGraphOf() — переиспользован и resolvedLightSources, и resolveControls/resolveIncomingControllers; grep не находит второй независимой reverse-graph реализации |
Подтверждено чтением |
| AC13 | npm test 838/838, включая нетронутые тесты #84/#94/#107; npm run build + сверка трёх SHA-256 |
Подтверждено |
| AC14 | git diff --stat origin/dev...HEAD: ни один файл custom_components/**/*.py, manifest.json, hacs.json, i18n не изменён; в diff нет optimistic-state (смок явно проверяет noOptimisticVisual), polling или новых network paths; lock/alarm invariant не затронут (Toggle-путь не касается secure targets) |
Подтверждено чтением |
Что проверено и корректно
- Один reverse-index (
incomingByMarkerвlightGraphOf) — единственный authority и дляsource.on, и для двух Toggle-путей (клик по linked source, клик по controller), как требует AC12/§8.2 ТЗ. - Lifecycle: добавление связи не стирает
off-bit (virtualLights.offне читается/не пишется в linked-режиме); снятие последней связи возвращает точное сохранённое значение — подтверждено и unit, и smoke. - Zero-driver случай — safe no-op (
command: null,configured-targets-missing) без отката кvirtual-light— именно то, что требует §9.2 ТЗ и AC5. - Документация (
docs/LIGHT.md,docs/USER-GUIDE.ru.md,docs/DEVICE-LIGHT-SETTINGS-MATRIX.ru.md, supersession-заметка вdocs/specs/107-virtual-light-toggle.md) переписана консистентно с новым контрактом и терминологией USER-GUIDE («Всегда», «Переключить состояние», «управляет другими источниками света»); оба changelog в том же коммите, чтоUser-Visible: yes. - Трейлеры: все четыре коммита несут
Issue: #174;User-Visible: yesтолько на коммите2f9cced, который и содержит оба changelog-файла в этом же коммите. Follow-up docs-коммитea2633a—User-Visible: no, что верно (только фингерпринт скриншотов). - Отсутствие изменений в
src/houseplan-card.ts— не пропуск, а следствие того, что confirmation/resolveToggleIntent-flow уже был общим механизмом с #94; проверено чтением вызывающего кода.
Находки
Нет High. Нет Medium. Нет Low.
Чего не проверял (см. таблицу гейтов выше для причин)
- Полный набор
demo/smoke_*.mjs(127 файлов) — не запускался, диапазон не задевает не затронутые поверхности. npm run golden:verify— не запускался; проверено чтением, что ни один golden-сценарий не использует затронутую конфигурацию.python -m pytest tests_backend -q— не запускался, backend не тронут.- Performance-профиль (
benchmark_large_houseи т.п.) — не запускался, budget не назван в AC, оставлен пре-релизному гейту. - Ручной проход через реальный
tap_confirm-диалог в браузере (AC8) — разобран по коду, не исполнялся как отдельный smoke; общий confirmation-механизм не получил специального сценария в новом smoke-файле.
Вердикт
Зелёный. Все 14 AC доказаны — unit-доказательства проверены на способность падать прямой мутацией кода, targeted smoke пройден живым запуском Chromium, типы/тесты/сборка зелёные, три копии бандла побайтово совпадают, трейлеры и changelog на месте. Продуктовое решение владельца (комментарий от 18.08.2026) реализовано без искажений: связанная пара ведёт себя как два умных устройства, HA state — единственный источник истины, несвязанный источник не регрессировал.