21 KiB
SPEC-REVIEW-251-r1
- Issue: #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 — верификация того, что заявленные в ТЗ факты о
текущем поведении не догадка, а точное описание существующего кода.
Как проверялось
- Прочитаны
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. - Прочитано тело issue #251 и все 6 комментариев (диагноз владельца, актуализация аналитики, claim-комментарий, вопрос Q1, решение владельца, хендофф на ревью).
- Гейты этапа 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. Оба совпадают с тем, что заявил автор в хендоффе.
- Построчная сверка технических утверждений ТЗ с кодом:
resolvePresentationSources()вsrc/device-presentation.ts:233-369— подтверждено, что для маркера сcontrolsvisualSources/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 на них не повисают в воздухе.
- Сверка терминологии с
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 найдены и сняты решением ревьюера с записью выше, без возврата автору) → зелёный. ТЗ готово к разработке.