Files
houseplan-card/docs/reviews/CODE-REVIEW-251-r1.md
T
2026-08-23 08:08:34 +03:00

17 KiB
Raw Blame History

CODE-REVIEW-251-r1

  • Issue: #251 — «Маркер выключателя гаснет, когда недоступна управляемая им лампа — читается как «выключатель offline»»
  • Этап: код-ревью (PROCESS.md §2.7), заход r1, блокирующих циклов израсходовано 0/4 до этого разбора
  • Ветка: issue/251-controller-target-availability
  • Проверяемый диапазон: origin/dev..HEAD
  • Коммиты в диапазоне:
    • ba0c787 docs(spec): separate controller and target availability — User-Visible: no
    • eb4f9aa docs: review document for #251 (артефакт спек-ревью, зелёный) — User-Visible: no
    • b984f16 fix: separate controller and target availability — User-Visible: yes
    • f18b5b4 docs: refresh screenshots for controller states — User-Visible: no
  • Спецификация: docs/specs/251-controller-target-availability.md, ТЗ-ревью зелёное (r1, IC_kwDOTOcLQM8AAAABQOr4Dg).
  • r1 — полный разбор, раздел «объём по дельте» (§2.10) неприменим.

Скоуп

Задача разделяет два факта, которые раньше вычислялись как одно: доступность физического контроллера (unavail/полупрозрачность) и working-состояние управляемой им цели (on/жёлтая подложка) для маркеров с marker.controls. Доступность контроллера теперь читается только из его собственных активных HA entity states (включая диагностические battery/linkquality/update), цели в неё не входят. Полностью недоступная configured-группа при явном Toggle теперь объясняет безопасный no-op локальным тостом с именем цели вместо молчаливого возврата; частично доступная группа продолжает исполнять доступное подмножество без тоста. Изменение Glow/fill/statistics, персистентной схемы, group-семантики «any on → all off» и нового визуального бейджа — вне скоупа и не тронуты.

Как проверялось

Дешёвые гейты (гоняются всегда, диапазон origin/dev..HEAD, HEAD f18b5b4):

Гейт Результат
npx tsc --noEmit OK, чисто
npm test # tests 1117 / # pass 1117 / # fail 0 (у автора в хендоффе — 1116 passed + 1 skipped; локально skipped не воспроизвёлся, на выводимость AC не влияет)
npm run build OK, dist/houseplan-card.js собран
cmp dist/houseplan-card.js custom_components/houseplan/frontend/houseplan-card.js и cmp dist/houseplan-card.js demo/srv/assets/houseplan-card.js совпадают, три копии идентичны
node scripts/check-docs.mjs --external (diff трогает src/**) Documentation checks passed (7 files, 10 external links)
node scripts/process-gate.mjs --range origin/dev..HEAD --issues гейт пройден, предупреждений 0

По необходимости, определяемой diff'ом и AC:

Гейт Результат
node scripts/smoke-select.mjs --base origin/dev --head HEAD Прямое совпадение (4): smoke_controls.mjs (← DevItem), smoke_help_affordance.mjs, smoke_optimize_coordinate_canonicalization.mjs, smoke_partition_openings.mjs (все три ← _showToast, широко используемый символ — слабая связь, но дешёвые, прогнал все)
node demo/smoke_controls.mjs OK, включая новые ключи unavailableTargets*, confirmRace* — все true
node demo/smoke_help_affordance.mjs OK
node demo/smoke_optimize_coordinate_canonicalization.mjs OK
node demo/smoke_partition_openings.mjs OK
node scripts/mutation-gate.mjs --id=controller-availability-follows-target controller-availability-follows-target: тест покраснел, как обязан (1/1)
node scripts/mutation-gate.mjs --id=controller-diagnostics-do-not-prove-online покраснел (1/1)
node scripts/mutation-gate.mjs --id=unavailable-toggle-stays-silent покраснел (1/1)
node scripts/mutation-gate.mjs --id=partial-group-shows-noop-toast покраснел (1/1)
node demo/golden/run.mjs --mode=capture --scenario=device-icon-state-table-light (diff меняет рендер: demo/golden/matrix.mjs правит уже забазленный сценарий) different — см. находку H1
node demo/golden/run.mjs --mode=capture --scenario=device-icon-state-table-dark different — см. находку H1

Полный npm run golden:verify (все ~40 сценариев) не гонял: policy.mjs требует полный прогон за один вызов (нет --scenario в verify-режиме), а два единственных сценария, которые правит diff, уже проверены точечно в diagnostic-capture-режиме напрямую против закоммиченных baseline-файлов — остальные 38 сценариев diff не касается ни по фикстурам, ни по коду рендера. Полный прогон — предрелизная обязанность (§8), не гейт этого ревью.

Не гонял и не было необходимости: python -m pytest tests_backend (diff не трогает custom_components/**/*.py), performance-профили (в AC не названы, чувствительный к перфу код не тронут — own-availability добавляет только O(e)-проход по уже построенному d.entities).

Находки

H1 (High, в скоупе) — golden-эталон device-icon-state-table-{light,dark} сломан этим же diff'ом, приёмки baseline нет

demo/golden/matrix.mjs в этом diff'е меняет уже забазленный сценарий device-icon-state-table-{light,dark}: маркеру golden-left-linkquality добавлен tap_action: 'toggle', controls: ['light.golden_light_three'], а light.golden_light_three переведён в state: 'unavailable'. Это ровно та матрица AC1/AC6 из ТЗ — доказательный сценарий, — но правка меняет визуальный результат уже принятого эталона, а сам эталон (demo/golden/baselines/ device-icon-state-table-{light,dark}.png) в этом diff'е не тронут и не переприниимался.

Воспроизведение (сборка — npm run build && cp dist/houseplan-card.js demo/srv/assets/houseplan-card.js на HEAD f18b5b4):

$ node demo/golden/run.mjs --mode=capture --scenario=device-icon-state-table-light
different         device-icon-state-table-light

artifacts/golden/golden-report.json: status: "different", diffRatio: 0.0013937744740160616 при пороге maxDiffRatio: 0.0005, maxObservedDelta: 221 при пороге maxChannelDelta: 10.

$ node demo/golden/run.mjs --mode=capture --scenario=device-icon-state-table-dark
different         device-icon-state-table-dark

diffRatio: 0.0014678027885700283, maxObservedDelta: 221 — тот же перебор порога.

Последствия:

  • job golden в .github/workflows/validate.yml идёт на каждый push в dev без путевых фильтров (needs.reuse.outputs.golden != 'true', а demo/golden/matrix.mjs входит в отпечаток reuse, значит кэш не переиспользуется) и уже существующий baseline есть → шаг вызывает npm run golden:verify, который упадёт на этом же расхождении. Слияние этого коммита в dev красит обязательный job тем же классом, что описан в контексте задачи для #230/#234, только для golden, а не docs.
  • docs/TESTING.md в этом же коммите утверждает доказательство AC6 как golden: device-icon-state-table light/dark — на деле этот golden сейчас красный, то есть AC6 (визуальный контракт View/kiosk/Static/preview один и тот же, без сдвига layout) не доказан визуально, только конфигурационно (test/golden-matrix.test.mjs проверяет только структуру сценария, не пиксели — сам тест это подтверждает, npm test зелёный именно поэтому).
  • Локальный гейт перед выходом из «В разработке» (PROCESS.md §8) явно требует golden:verify, «если менялся визуал» — здесь менялся по определению (сам diff это и добавляет), но команда отсутствует в хендофф-комментарии автора среди прогнанных гейтов.

Это не претензия к корректности самого расчёта доступности — юнит-матрица (test/device-presentation.test.mjs) и её мутанты подтверждают, что новое поведение соответствует ТЗ. Дефект в поставке: изменение уже принятого эталона без сопутствующей приёмки нового, что ломает обязательный CI-гейт на dev.

Как чинить, не задевая процесс: эталон нельзя просто перезаписать локально (§3.13, §12: «принятие golden-эталонов ради зелёного CI» запрещено) — нужен полноценный npm run golden:accept -- --reviewed по прогону golden job на полном Linux CI-артефакте той же ветки, аналогично тому, как коммит f18b5b4 принял provenance скриншотов документации по прогону 32617490601. До этого коммита — issue не может уйти дальше «В разработке».

Блокирует. Возврат автору.

Что проверено и корректно

  • AC1/AC2 (матрица §6.1, разделение фактов). controllerAvailability() в src/device-presentation.ts:218-234 читает только d.entities, живое состояние — не пустая строка, не unknown, не unavailable; virtual-девайс всегда available. Применяется только когда sources.sourceKind === 'controls', оставляя combined.status (working/alarm) нетронутым. Новый тест issue 251 separates controller availability from controlled target status (test/device-presentation.test.mjs) прогоняет ровно матрицу из ТЗ: battery/LQI живые + target unavailable → available+neutral; target on → working при доступном контроллере; все own diagnostics unavailable + target on → unavail имеет приоритет над working; live_states:false → нейтраль; event-only own → unavailable; virtual controller → available; критический alarm own-сущности сохраняет приоритет (status: 'alarm', availability: 'available'). Мутанты controller-availability-follows-target и controller-diagnostics-do-not-prove-online покраснели, как обязаны.
  • AC3/AC4 (тост при полностью недоступной группе, единственная/множественная цель, mixed secure/unsupported). unavailableToggleTargetNames() (src/device-toggle.ts:769-796) фильтрует по kind==='group' + noneReason==='configured-targets-missing', называет только missing/ha-disabled/unavailable, глушится при любом unsupported в пропусках, не называет secure. Юнит issue 251 classifies only unavailable configured groups... покрывает singular/plural/partial/exact binding/mixed-secure/mixed-unsupported — все ветки ТЗ §7.1. Прод-бандл smoke (smoke_controls.mjs) подтверждает: нет callService/callWS, нет press feedback (_devicePressAnimations.size === 0), нет открытия confirm/info card, точный текст тоста для singular и plural. Мутант unavailable-toggle-stays-silent покраснел.
  • AC5 (confirm race). houseplan-card.ts:4972-4977: перед сравнением target-set повторно резолвит intent и, если !toggleOperation(current) и _showUnavailableToggleTargets(current) вернул true (т.е. это именно наш no-op класс), показывает unavailable-тост и не идёт дальше; иначе (secure, unsupported, обычная смена состава) — прежний toast.tap_target_changed, поведение не регрессирует (прочитано и сверено построчно со старой веткой, smoke confirmRaceNoService/confirmRaceUnavailableToast — true).
  • AC7 (config/schema/model не меняются). В diff'е нет файлов схемы/модели (src/types.ts, backend, персистентный формат конфигурации); существующие config-тесты (входят в те же 1117) зелёные без изменений в них.
  • AC8 (документация и i18n). docs/ARCHITECTURE.md, docs/USER-GUIDE.md и .ru.md, docs/TESTING.md, оба docs/CHANGELOG*.md — правки в том же коммите b984f16 (User-Visible: yes), формулировки соответствуют реализации (сверено построчно). test/i18n.test.mjs подтверждает точное совпадение EN/RU текста тоста и наличие обоих ключей в cardSource. check-docs.mjs --external зелёный.
  • AC9 (implementation loop, bundle parity) — см. таблицу гейтов выше, все три копии бандла идентичны байт в байт.
  • Трейлеры/провенанс. Все три продуктовых коммита несут Issue: #251 и ровно один User-Visible:; b984f16 — yes с правками в обоих changelog в том же коммите; process-gate.mjs --issues зелёный.
  • Скриншоты документации. f18b5b4 ссылается на прогон Docs screenshots run: 32617490601 и обновляет docs/images/* + screenshots.json; check-docs.mjs (без --external, полная проверка provenance) зелёный — отпечаток соответствует текущему src/**.

Чего не проверял

  • Полный npm run golden:verify (все ~40 сценариев) и полный npm run golden:capture для остальных сценариев — не задеты ни фикстурой, ни кодом рендера этого diff'а; это предрелизная, а не ревью-обязанность (§8).
  • python -m pytest tests_backend — diff не трогает custom_components/**/*.py.
  • performance-профили — не названы в AC, presentation-путь остаётся O(e) по уже построенному d.entities (проверено чтением, не исполнением).
  • Реальную живую HA-инсталляцию/ручной клик в браузере вне smoke-гарнеса — ручного тестирования в цикле нет по правилам процесса; вопрос «работает ли» закрыт сочетанием юнит-матрицы, мутантов и production-bundle smoke.
  • Провенанс CI-прогона 32617490601 (сам workflow run на GitHub) — доверился структурной проверке check-docs.mjs, содержимое run'а не открывал.