Files
houseplan-card/docs/reviews/CODE-REVIEW-174-r1.md
T
2026-08-18 17:29:48 +00:00

143 lines
14 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Код-ревью 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 | — | пусто, сгенерированные копии не разошлись с закоммиченными |
**Дисциплина «тест умеет падать» — не поверье, а проверено мутацией:**
1. В `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 в этом файле).
2. В `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 зелёные.
3. После обоих восстановлений — полный `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 — единственный источник истины, несвязанный источник не
регрессировал.