22 KiB
SPEC-REVIEW-103-r1
- Issue: https://github.com/Matysh/houseplan-card/issues/103
- ТЗ под ревью:
docs/specs/103-toggle-confirmation-state.md(коммит49c0c3d, веткаissue/103-toggle-confirmation-state) - Роль: ревьюер ТЗ (не автор), этап
S4-spec-review - Трек: обычный (не
small/trivial) — согласно аналитике владельца от 2026-08-14: сложность 2/10, риск 3/10, ноtrivialнамеренно не применён, так как confirmation copy — UX-контракт, потенциально затрагивающий несколько доменов (power/cover/valve/group/virtual light) - Цикл: r1/4
Скоуп ревью
Проверялось соответствие ТЗ:
docs/SCOPE.md— попадание задачи в Core user jobs, отсутствие расширения скоупа и конфликта с «lock invariant»;PROCESS.md§2.4, §2.5 (DoR), §7.1 (обязательные разделы), §3/§12 (запреты), §5 (легкий трек — не применяется, но проверено, что признакиsmallдействительно отсутствуют);AGENTS.md— классы файлов коммита, ветка, трейлеры;- фактическому состоянию кода
src/device-toggle.tsиsrc/houseplan-card.ts— технические утверждения ТЗ о существующем резолвере #94 (ResolvedToggleIntent,formatToggleIntent,sameToggleOperationTargets,_tapConfirm) сверены построчно, чтобы отличить факт от догадки; docs/USER-GUIDE.ru.md— терминология («Переключить состояние», hint под селектором действия,tap_confirm);docs/TOUCH-SUPPORT.md— категория «View dialogs and safe device actions» (fully supported на touch, не best-effort).
Как проверялось
- Прочитан весь тред issue #103: исходное тело (уже содержит нормативную
матрицу, i18n-ключи и race-контракт от владельца), комментарий аналитики
2026-08-15 (
P3, сложность 2/10, риск 3/10, «вопросов нет»), комментарий автора ТЗ («продуктовых вопросов нет, метка остаётсяS3-specпо поручению владельца» — на момент ревью метка ужеS4-spec-review, расхождение не процессное:docs/specs/README.mdподтверждает коммит и ветку). - Сверены обязательные разделы ТЗ (§7.1 PROCESS.md) построчно — таблица ниже.
- Прочитан
src/device-toggle.tsцеликом: подтверждено существованиеResolvedToggleIntent,ToggleNextEffect,formatToggleIntent(),sameToggleOperationTargets(),toggleOperation()— ровно тот резолвер #94, на который ссылается ТЗ, а не придуманный интерфейс. - Прочитан
src/houseplan-card.ts(окрестности_clickDevice,:4336-4400и:14838-14846): подтверждено дословно, что текущий_tapConfirm—{ text: string; exec: () => void }, аtextстроится только какthis._t('confirm.tap_toggle', { name })— то есть претензия ТЗ «диалог показывает только имя» верна для текущего кода, не выдумана. - Прочитан
_toggleHintLines()(:17314-17358) и соответствующие ключиmarker.toggle_hint_current/marker.toggle_effect_*/marker.toggle_hint_group_current/marker.toggle_hint_skippedвsrc/i18n/ru.jsonиen.json. Эта существующая инфраструктура уже строит «текущее → эффект» для редакторского hint под селектором действия (docs/USER-GUIDE.ru.md:489). ТЗ не игнорирует её: §5 явно допускает расширениеformatToggleIntent()либо добавление соседней pure-функции, а разделение current/expected на отдельные строки в §8 обосновано собственным accessibility-требованием (порядок чтения screen reader'ом), которого нет у однострочного hint. Дублирования семантики без причины не обнаружено. - Проверена логика
coverLikeService()(:307-331) против нормативной матрицы §6 ТЗ — см. Low-3. - Прочитан
test/device-toggle.test.mjsи списокdemo/smoke_*.mjs: подтверждено существованиеsmoke_ha_controls.mjs,smoke_controls.mjs,smoke_virtual_light_toggle.mjs, на которые ссылается план регрессии §11 — не выдуманные имена. - Прочитан
docs/TOUCH-SUPPORT.md: строка «View dialogs and safe device actions» — «Fully supported» на touch (не best-effort), то есть touch- влияние здесь блокирующее по DoR; ТЗ §8 адресует это узкой mobile-footer и narrow-viewport smoke — соответствует канону. - Прочитан
docs/USER-GUIDE.ru.md:489и «Краткая памятка безопасности» (:1206): термин «Переключить состояние» и рекомендация включать подтверждение для Toggle/Run/Cover совпадают с ТЗ дословно. - Проверена трассируемость:
docs/specs/README.mdполучил секцию## P3со строкой на #103 в том же коммите;git diff --stat origin/dev...HEADпоказывает только два файла класса C; коммит49c0c3dнесётIssue: #103иUser-Visible: no— корректно для документа ТЗ, который сам не меняет поведение.
Обязательные разделы (§7.1 PROCESS.md)
| Раздел | Есть | Комментарий |
|---|---|---|
| Сценарий (персона/поверхность/момент) | Частично | §1 объясняет проблему технически (только имя в диалоге), но ни разу не называет персону/поверхность явной фразой — см. Low-1 |
| Что человек увидит до/после | ✅ | §2, буквально в виде текста диалога «до/после» |
| Проблема | ✅ | §1 |
| Скоуп / не-скоуп | ✅ | §3 / §4 |
| Контракт поведения | ✅ | §5, §6, §9 |
| Модель данных и миграция | ✅ | §7 (TS-интерфейс _tapConfirm) + §9 («config не меняются», миграции нет) |
| UX и i18n | ✅ | §8 |
| AC1…ACn с доказательством | Частично | §10 — 10 пронумерованных AC, но без построчной метки типа доказательства; тип восстанавливается однозначно из §11 (см. Low-2) |
| План автотестов | ✅ | §11, разбит на unit/integration/регрессию |
| Риски | ✅ | §14 |
| Откат | ✅ | §14 (последний абзац) |
| Release-артефакты | ✅ | §13 |
Дополнительно есть раздел «Принятые технические предположения» (§15) — не требуется §7.1 буквально, но прямо соответствует духу PROCESS.md §7.1 о записи технических решений отдельным блоком.
Находки
Находок уровня High и Medium нет — отдельные issue заводить не требуется.
Low-1 — сценарий не называет персону/поверхность явной фразой
Файл: docs/specs/103-toggle-confirmation-state.md (§1)
AGENTS.md требует, чтобы первый раздел ТЗ отвечал: какая персона (из
docs/SCOPE.md), на какой поверхности, в какой момент встретит изменение.
§1 объясняет техническую проблему («текущий диалог показывает только имя»),
но не пишет explicit: диалог tap-confirm — это «View dialogs and safe device
actions» (TOUCH-SUPPORT.md), доступный любой персоне на любой поверхности
(desktop/kiosk/companion app) в момент нажатия на маркер с tap_confirm.
Смысл восстанавливается из §2 (пример текста диалога) и общего контекста
J3 SCOPE.md, поэтому неоднозначности для читателя нет, но формальное
требование выполнено не буквально.
Решение ревьюера: Low, не блокирует. Косметическая правка одним предложением на усмотрение автора при следующей редакции.
Low-2 — AC1–AC10 не промаркированы построчно типом доказательства
Файл: docs/specs/103-toggle-confirmation-state.md (§10, AC1–AC10)
DoR (PROCESS.md §2.5) и цепочка §7.1 требуют, чтобы «у каждого [AC] указано,
чем он доказывается: unit / backend / smoke / golden / ревью кода». §10
перечисляет десять проверяемых критериев, но ни один не несёт явной метки
доказательства — план тестирования (§11) даёт эти метки только группами
(Unit / Integration-browser / Регрессия), и сопоставление 1:1 нужно
восстанавливать вручную. Я проверил это сопоставление вручную: AC1–AC6
однозначно закрываются перечисленными в §11 unit-кейсами (formatter per
ToggleNextEffect, single/group/partial/unknown/no-operation), AC7–AC8 —
race-кейсами из Integration/browser, AC9 — DOM order/narrow viewport/keyboard
smoke, AC10 — «run confirmation unchanged» той же секции. Ни один AC не
остаётся недоказуемым по существу; отсутствует только явная метка в самом
тексте §10. Тот же класс находки (типы доказательства AC вне буквального
формата) уже фиксировался как Low и не блокировал приёмку в
SPEC-REVIEW-137-r1 (Low-3).
Решение ревьюера: Low, не блокирует. Рекомендуется при следующей правке
приписать к каждому AC1–AC10 короткую метку (unit/integration/review кода), чтобы код-ревью могло сверяться механически, а не восстанавливать
сопоставление заново — но проверяемость критериев уже подтверждена этим
документом.
Low-3 — нормативная матрица §6 группирует cover-состояния не так, как резолвер
Файл: docs/specs/103-toggle-confirmation-state.md:78-79 (§6, строки
«cover closed/closing + open» и «cover + stop»)
Код: src/device-toggle.ts:307-331 (coverLikeService)
Таблица группирует «closed/closing» в одну строку с ожидаемым open. Но по
факту резолвера состояние closed действительно даёт nextEffect: 'open',
а closing — отдельную ветку nextEffect: 'stop' (наравне с opening),
то есть реальная комбинация «closing → next open» в резолвере никогда не
возникает: она относится к строке «cover + stop» этой же таблицы. Так как
формула §5 явно требует брать current-строку из HA-formatted state, а не
пересчитывать её из этой таблицы, и направление всегда берётся из
nextEffect резолвера (а не выводится реализацией по названию строки), риска
для рантайма нет — но группировка вводит в заблуждение при написании unit-
теста по этой таблице буквально (можно ошибочно закодировать несуществующую
комбинацию state=closing + effect=open как «нормальный» кейс вместо
state=closing + effect=stop).
Решение ревьюера: Low, не блокирует. При следующей редакции стоит убрать
closing из строки open (оставить только closed) и явно отметить, что
строка stop покрывает оба переходных состояния (opening/closing) — но
это не меняет ни один AC и не требует возврата цикла.
Что проверено и корректно
- Соответствие
docs/SCOPE.md: задача уточняет содержимое существующего Toggle-confirmation guard'а — это J3 («Let me act on the obvious right from the plan», guarded quick actions), статус Closed; расширения продукта нет, задача улучшает уже принятую функциональность и не создаёт нового actuation-пути. Явно проверено, что «lock invariant»SCOPE.mdне задет: secure-цели (lock.*,alarm_control_panel.*, guarded cover) уже фильтруются резолвером на уровнеsecureEntity()и никогда не попадают вtargets, следовательно новый confirmation-текст не может обещать переключение замка. - Легитимность полного трека: несмотря на низкую оценку сложности (2/10) от
владельца, автор корректно не применил
trivial/small— confirmation copy реально затрагивает пять разных семантик (power/cover/valve/group/ virtual light) и i18n; критерий «одна поверхность» (§5 PROCESS.md) не выполняется буквально, обычный трек оправдан. - Продуктовых вопросов владельцу нет, и это корректно: почти вся нормативная
матрица уже была продиктована владельцем в теле issue до написания ТЗ,
автор её только формализовал и уточнил (см. Low-3 как единственное реальное
уточнение, не вопрос). Ни одна догадка не выдана за факт без пометки —
§15 отдельно фиксирует три технических предположения (no live-update
snapshot, HA-formatter приоритет,
stopкак честный ожидаемый эффект), все они соответствуют либо явному тексту issue, либо существующему коду. - Технические утверждения о текущем коде подтверждены чтением, а не
голословны:
ResolvedToggleIntent/formatToggleIntent/sameToggleOperationTargets(#94,src/device-toggle.ts), текущий однострочный_tapConfirm.text(src/houseplan-card.ts:4388-4389), существующий toasttoast.tap_target_changed, существующие тестыtest/device-toggle.test.mjsи смокиsmoke_ha_controls.mjs/smoke_controls.mjs/smoke_virtual_light_toggle.mjs. - Не-скоуп (§4) корректно отсекает смежные соблазны: изменение resolver/
target-selection/service-call, прогноз scripts/scenes, история состояний,
redesign confirmation-диалогов удаления/unlock/run, live-анимация в
открытом диалоге, изменение схемы
tap_confirm— типичные места, где скоуп мог бы незаметно расшириться, явно исключены. - Touch-контракт: диалог tap-confirm — категория «View dialogs and safe
device actions» (
TOUCH-SUPPORT.md), полностью поддерживаемая на touch (не best-effort). ТЗ §8 адресует это явно (narrow footer с двумя кнопками, перенос длинного текста без horizontal scroll) и §11 добавляет narrow- viewport smoke — соответствует блокирующей категории канона. - Совместимость:
stored tap_confirmи схема config не меняются, миграции нет — корректно для confirmation copy, не меняющей контракт хранения. Откат (§14) корректно опирается на то, что_tapConfirmвозвращается к одной строке без отдельного data rollback. - i18n: ключи перечислены раздельно для RU/EN в одном release-коммите (§13);
plural rules осознанно не вводятся, что соответствует «counter-safe style»
уже принятому в существующих
marker.toggle_*строках. - Release-артефакты (§13) корректно относят golden/screenshot к точечному необязательному кадру, а backend/migration/performance/security — явно «не требуются», что соответствует характеру изменения (чистый UI-текст без новых сервис-вызовов).
- Трассируемость:
docs/specs/README.mdобновлён тем же коммитом (новая секция## P3со строкой на #103), ссылка issue ↔ ТЗ двусторонняя; веткаissue/103-toggle-confirmation-stateи трейлеры коммита (Issue: #103,User-Visible: no) корректны для документа класса C, который сам не меняет поведение. git diff --stat origin/dev...HEADне содержит ни одного файла класса A — код не тронут доS5-ready, что соответствует правилу №1.
Чего не проверял
- Не проверял реализуемость
_tapConfirmкак размеченного union ({kind:'toggle', ...} | {kind:'run', ...}) на уровне TypeScript-типов — это по правилам ТЗ свободно изменяемое техническое решение автора кода (§15), не предмет ревью ТЗ. - Не запускал автотесты, build или browser-смоки — на этапе
specэто не требуется; существование резолвера, hint-инфраструктуры, i18n-ключей и тестовых/смоук-файлов, на которые ссылается ТЗ, проверено чтением исходников, а не исполнением. - Не проверял корректность численных оценок аналитики (ценность 5/10 и 3/10, сложность 2/10, риск 3/10, P3) по существу — это поле владельца (PROCESS.md §2.2), уже принятое явным решением до написания ТЗ.
- Не проверял, как именно
hass.formatEntityStateповедёт себя для каждого конкретного домена/state на реальном HA — ТЗ корректно требует safe fallback на raw state, и это уже проверяемый unit-кейс по плану §11, а не вопрос ревью ТЗ.
Вердикт
Зелёный. High: 0, Medium: 0. Три находки Low (сценарий не называет
персону/поверхность явной фразой; AC1–AC10 не промаркированы построчно типом
доказательства, хотя проверяемость подтверждена этим документом; нормативная
матрица §6 вводит в заблуждение группировкой cover closed/closing в строке
open, хотя резолвер направляет closing в строку stop) — ни одна не
блокирует приёмку и не меняет ни один AC; все три либо правятся косметически
при следующей редакции, либо снимаются этой записью без нового цикла.