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

238 lines
21 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.
# SPEC-REVIEW-251-r1
- Issue: [#251](https://github.com/Matysh/houseplan-card/issues/251) — «доступность контроллера не наследуется от управляемой цели»
- Этап: ТЗ на ревью (PROCESS.md §2.4)
- Заход: r1 · блокирующих циклов израсходовано 0 из 4 (первый раунд, разбор полный)
- Артефакт ТЗ: `docs/specs/251-controller-target-availability.md` (355 строк), запись в `docs/specs/README.md:130`
- Ветка: `issue/251-controller-target-availability`
- Ревьюер получил issue и ТЗ без устных пояснений автора; ниже — независимая проверка по коду `dev`.
## Скоуп ревью
Диапазон `origin/dev..HEAD` — один коммит `ba0c787` («docs(spec): separate
controller and target availability»), трейлеры `Issue: #251` /
`User-Visible: no` на месте, класс C (документация), правка сама по себе не
требует `User-Visible: yes`. Изменены только `docs/specs/251-*.md` и
`docs/specs/README.md`. Продуктовый код (`src/**`) не тронут — это ожидаемо
для этапа ТЗ.
Предмет ревью: полный текст ТЗ против §7.1 PROCESS.md (обязательные разделы),
против диагноза в теле issue и трёх комментариев владельца/аналитика, и против
реального кода `src/device-presentation.ts` / `src/device-toggle.ts` /
`src/houseplan-card.ts` — верификация того, что заявленные в ТЗ факты о
текущем поведении не догадка, а точное описание существующего кода.
## Как проверялось
1. Прочитаны `docs/SCOPE.md`, `AGENTS.md`, `PROCESS.md` §2.4/§2.9/§7.1/§7.2,
`docs/USER-GUIDE.ru.md` (разделы «Жесты маркера», «Действия», «Визуальные
состояния устройств», §12), `docs/TOUCH-SUPPORT.md`.
2. Прочитано тело issue #251 и все 6 комментариев (диагноз владельца,
актуализация аналитики, claim-комментарий, вопрос Q1, решение владельца,
хендофф на ревью).
3. Гейты этапа spec:
- `node scripts/check-docs.mjs --external` → `Documentation checks passed
(7 files, 10 external links)` — green;
- `node scripts/process-gate.mjs --range origin/dev..HEAD --issues` →
`гейт пройден, предупреждений 0` — green.
Оба совпадают с тем, что заявил автор в хендоффе.
4. Построчная сверка технических утверждений ТЗ с кодом:
- `resolvePresentationSources()` в `src/device-presentation.ts:233-369` —
подтверждено, что для маркера с `controls` `visualSources`/`samples`
строятся исключительно из `lights` (controls-derived + owned light
sources) и **никогда** не читают `d.entities` устройства-контроллера
(battery/LQI/event туда не попадают в принципе, кроме ветки `alarm` в
`criticalSources`). Это точное описание дефекта, не догадка.
- `resolveDevicePresentation()` (там же, 580-711) — `visual =
combineVisualSamples(sources.samples)`, класс `unavail` берётся из
`visual.availability === 'unavailable'` (строка 564) раньше, чем
`working`/`open` — подтверждает утверждение ТЗ §6.1 о приоритете
`unavail` над `working`.
- `src/device-toggle.ts`: `SkippedToggleTarget{ref, entityId, name, reason}`
(37-58), `toggleOperation()` (763-767) возвращает `null`, когда нет
`command`/`operation` — подтверждает опору AC3/AC4/AC5 на реальную,
уже существующую классификацию, а не на новый API.
- `resolveOwnEntity()`/`reasonForSingle()` (405-497) — для одиночной цели
`noneReason` уже мапит `missing → 'unavailable'`, `skippedTargets`
сохраняет причину и имя — ровно то, что нужно для локализованного
сообщения без нового запроса к HA (заявление аналитика подтверждено).
- `_clickDevice()` в `src/houseplan-card.ts:4930-4979` — строка 4932,
`if (!initial || !toggleOperation(initial)) return; // ... quiet no-op`
— это именно тот тихий `return`, который ТЗ называет дефектом второй
части (п.3 «Актуализации аналитики»). Подтверждено чтением, не
исполнением.
- Токст toasta: `src/houseplan-card.ts:16473,17069` —
`<div class="toast" role="alert" aria-live="assertive">` — ТЗ §7.1/§8
точно описывает существующую разметку (после первичной проверки я
нашёл в коде другой, необязательный `role="status" aria-live="polite"`
у `_renderRecoveryOverlay()`, 3389-3399 — это другой компонент,
оверлей восстановления связи, не toast; ложная тревога снята).
- `confirm.unavailable_targets` (`src/i18n/ru.json:722`,
`src/houseplan-card.ts:19805`) — существующий паттерн «Недоступно:
{count}» без словоформ; новый текст ТЗ («Цели недоступны: {names}»)
грамматически не зависит от числа и не наследует эту проблему —
формулировка ТЗ безопасна для RU-множественного числа.
- `isControllable()` (`src/logic.ts:1657-1658`) ограничивает прямые
ссылки `controls` доменами `light`/`switch`; `secureEntity()`
(`device-toggle.ts:209-218`) относится к `lock`/`alarm_control_panel`/
охраняемым классам `cover` — по коду смешение `secure` с
`unavailable/missing` в одной группе `controls` технически достижимо
только через `marker:`-алиас на **другой** маркер, помеченный
`is_light: true` (`_controlCandidates`, `houseplan-card.ts:19975-19992`
фильтрует кандидатов именно так) — узкий, нетипичный путь конфигурации.
- Виртуальные маркеры (`devices.ts:1246-1265`) всегда получают
`entities: []` — значит строка матрицы §6.1 «virtual controller без
own entities» — это не частный случай, а единственно возможный для
`virtual: true`; formulировка §5 «виртуальные контроллеры... как
всегда доступные» не создаёт скрытого противоречия с матрицей.
- `demo/smoke_controls.mjs`, `test/device-presentation.test.mjs`,
`test/device-toggle.test.mjs` существуют — ссылки AC1/AC3/AC4 на них
не повисают в воздухе.
5. Сверка терминологии с `docs/USER-GUIDE.ru.md`: текущая документированная
формулировка «Если все сущности, реально описывающие маркер, недоступны,
маркер бледнеет» (строка 855-857) и «Бледный маркер | Данные недоступны...
| Все рабочие сущности маркера unknown, unavailable или отсутствуют»
(871-875) — это ровно контракт, который ТЗ №251 сознательно меняет
(разделяет «рабочие сущности» на own-controller и target). ТЗ п.14
(«Release-артефакты») правильно требует правку `USER-GUIDE.md`/`.ru.md` в
том же коммите реализации — иначе документация и код разойдутся, как уже
было отмечено process-риском в самом ТЗ (риск 6).
## Находки
Обе найденные точки — низкой серьёзности; ни одна не блокирует и не
требует возврата автору. Записываю решение ревьюера по каждой (PROCESS.md
§3.8: Low либо правится, либо снимается решением ревьюера с записью).
### L1 — не проговорено соответствие toast'а формулировке владельца «стандартными средствами HA»
Первый комментарий владельца в issue: «...выводить корректное сообщение...
**стандартными средствами HA** (как пишется, что соединение с HA потеряно)».
ТЗ §7.1/§8 отвечает на это локальным тостом самой карточки House Plan
(`role="alert" aria-live="assertive"`), а не нативным механизмом HA
(например, событием `hass-notification`, которым сама HA показывает баннер
о потере соединения). ТЗ не проговаривает это расхождение и не отмечает
выбор как намеренную интерпретацию.
**Почему не Medium:** в кодовой базе уже существует устоявшийся паттерн —
все аналогичные «действие не выполнено» сообщения (`toast.error`,
`toast.tap_target_changed`, `toast.no_entity`, `toast.ha_disabled_action`,
`toast.run_target_missing`) используют именно локальный тост карточки, а не
нативный HA-механизм. Новое сообщение, реализованное иначе, было бы
единственным исключением и визуально разошлось бы с остальной картой.
Фраза владельца «как пишется, что соединение с HA потеряно» по контексту
описывает регистр/тон сообщения («это системное объяснение, а не тихая
ошибка»), а не требует буквально вызвать HA-нативный баннер; последующие
три коммента владельца (Q1 и решение) не возражают против уже
предложенного тостового решения аналитика.
**Решение ревьюера:** снимаю как Low. Прошу автора добавить в §7.1 или §17
одну строку, явно фиксирующую, что «стандартные средства HA» реализуются
локальным тостом карточки по образцу уже существующих `toast.*`-сообщений —
это превратит implicit-решение в явно записанное предположение и закроет
дорогу к спору на код-ревью.
### L2 — не решён смешанный `secure` + `unavailable` случай в группе `controls`
§7.1 формулирует условие тоста как «все пропуски объясняются `unavailable`,
`missing` или `ha-disabled`» и отдельно говорит, что no-op из-за `secure`
сохраняет существующее (тихое) поведение. Не описан случай, когда в одной
группе `controls` одновременно есть и `secure`-цель, и `unavailable`/
`missing`-цели, при нулевом исполняемом множестве: он не подходит под
условие «все пропуски объясняются unavailable/missing/ha-disabled» (раз
среди них есть secure), но и не подходит под «no-op вызван secure»
(раз среди них есть не-secure причины). Естественная реализация на базе
существующего `noneReason` (`device-toggle.ts:536-537`, который различает
только `'secure'` при **всех** secure и `'configured-targets-missing'` во
всех остальных случаях) молча покажет тост и для этого смешанного случая —
поведение, о котором ТЗ прямо не высказывается.
**Почему не Medium:** по коду смешение достижимо только через `marker:`-
алиас на другой маркер с `is_light: true`, а не через прямую ссылку на
`lock.*`/`alarm_control_panel.*` (те отфильтрованы `isControllable()` уже на
этапе выбора в редакторе). Обычный охраняемый `cover` (garage/door/gate)
почти никогда не выставляется как `is_light: true`, так что путь требует
нетипичной ручной конфигурации. Независимо от исхода замок/охранная панель
не получает command — инвариант `SCOPE.md` («никогда не по тапу на плане»)
не затрагивается ни при каком прочтении.
**Решение ревьюера:** снимаю как Low. Достаточно одной уточняющей строки в
§7.1 или §16 (риски): смешанная группа classифицируется так же, как
«все пропуски unavailable/missing/ha-disabled» — тост показывается, имена
secure-целей в тексте тоста не участвуют отдельным исключением. Не блокирует
разработку; можно поправить в рамках реализации без повторного цикла ревью
ТЗ.
## Что проверено и корректно
- Обязательные разделы §7.1 PROCESS.md присутствуют и в правильном порядке:
сценарий/персона (§1) → что человек видит до/после (§2) → диагноз (§3) →
решение владельца (§4) → скоуп/не-скоуп (§5) → контракт presentation (§6) →
контракт действия (§7) → UX/a11y/touch (§8) → данные/compat/migration (§9) →
perf/security (§10) → AC1-AC9 с доказательством (§11) → план реализации и
автотестов (§12) → mutation guards (§13) → release-артефакты (§14) →
откат (§15) → риски (§16) → явный блок принятых технических предположений
(§17).
- Продуктовые разделы 1-2 отвечают на «какая персона/поверхность/момент» и
«что человек видит одной фразой без терминов реализации» — оба
недвусмысленны.
- Единственный продуктовый вопрос (Q1 — как считать доступность самого
контроллера при отсутствии device-level state в HA) задан владельцу пачкой,
с предлагаемым default, `blocked` был выставлен и снят после ответа —
процесс §2.3/§7.1 соблюдён буквально. «Продуктовых вопросов не осталось»
(§4 ТЗ) подтверждается тем, что все решения §4 действительно восходят к
цитируемым словам владельца, а не к догадке автора.
- Матрица §6.1 однозначна и проверяема: каждая строка задаёт конкретный вход
(состояния own/target) и конкретный выход (availability/status/класс),
привязана к AC1 и к перечисленному mutation guard
(`controller-availability-follows-target`,
`controller-diagnostics-do-not-prove-online`).
- Скоуп/не-скоуп (§5) взаимно непротиворечивы и совпадают с решением
владельца (нет нового бейджа, Glow не меняется, групповая семантика «any on
→ all off» не меняется, secure-инвариант не трогается).
- Модель данных/миграция (§9) — корректно «нет миграции»: правка чисто
presentation/action, персистентная схема `marker.controls` не меняется,
подтверждено тем, что все затронутые функции читают, а не переписывают
`d.entities`/`d.controls`.
- Все AC (1-9) пронумерованы и у каждого указан способ доказательства
(`unit`/`smoke`/`golden`/«ревью кода»/config diff) — требование DoR §2.5
выполнено.
- i18n: точный RU/EN текст обеих реплик дан в §7.1; именование конкретных
ключей оставлено автору как техническая деталь (не наблюдается
пользователем) — это в рамках §7.1 PROCESS.md («именование... агенты
решают сами»), не дефект готовности к разработке.
- Touch/a11y (§8): не вводит новых жестов, использует существующий
keyboard/tap путь и объявление screen reader — согласуется с
`TOUCH-SUPPORT.md` (View/kiosk — гарантированная touch-поверхность) и не
создаёт editor-специфичного расхождения.
- Откат (§15) реалистичен: одна code revision, без обратной миграции данных.
- Release-артефакты (§14) перечисляют оба changelog, `ARCHITECTURE.md`,
оба `USER-GUIDE`, `TESTING.md` — учитывая, что USER-GUIDE.ru.md сейчас
прямо документирует старый (заменяемый) контракт «Бледный маркер», это
необходимое требование, а не избыточное.
- Гейты spec-этапа (`check-docs`, `process-gate`) перезапущены ревьюером
независимо и совпали с заявленными автором результатами.
## Чего не проверял
- Не запускал `npm run typecheck` / `npm test` / `npm run build` — диапазон
`origin/dev..HEAD` не содержит изменений `src/**`/`test/**` (только
`docs/specs/**`), эти гейты на этапе ТЗ не применимы к нулевому
код-диффу; они станут обязательными в код-ревью реализации.
- Не проверял `demo/smoke_controls.mjs` и `test/device-presentation.test.mjs`
/ `test/device-toggle.test.mjs` на способность падать под будущими
мутантами — на этапе ТЗ кода мутантов ещё нет; это станет предметом
код-ревью (раздел §13 «Mutation guards» ТЗ уже задаёт, что именно должно
покраснеть).
- Не проверял golden/скриншот-провенанс — на этапе ТЗ `src/**` не менялся,
`check-docs.mjs --external` уже подтвердил, что текущий provenance зелёный.
- Не оценивал реальную Zigbee/HA-инсталляцию из исходного репорта (батарея
100, LQI 164, группа из четырёх лампочек) — это внешний живой стенд
владельца, недоступный ревьюеру; диагноз проверен по коду, а не повторным
воспроизведением на инсталляции.
## Вердикт
High: 0 · Medium: 0 (два Low найдены и сняты решением ревьюера с записью
выше, без возврата автору) → **зелёный**. ТЗ готово к разработке.