17 KiB
CODE-REVIEW-251-r1
- Issue: #251 — «Маркер выключателя гаснет, когда недоступна управляемая им лампа — читается как «выключатель offline»»
- Этап: код-ревью (PROCESS.md §2.7), заход r1, блокирующих циклов израсходовано 0/4 до этого разбора
- Ветка:
issue/251-controller-target-availability - Проверяемый диапазон:
origin/dev..HEAD - Коммиты в диапазоне:
ba0c787docs(spec): separate controller and target availability —User-Visible: noeb4f9aadocs: review document for #251 (артефакт спек-ревью, зелёный) —User-Visible: nob984f16fix: separate controller and target availability —User-Visible: yesf18b5b4docs: 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; targeton→ working при доступном контроллере; все own diagnostics unavailable + targeton→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, поведение не регрессирует (прочитано и сверено построчно со старой веткой, smokeconfirmRaceNoService/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'а не открывал.