Files
houseplan-card/docs/reviews/SPEC-REVIEW-103-r1.md
T
2026-08-19 03:12:56 +03:00

258 lines
22 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-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).
## Как проверялось
1. Прочитан весь тред issue #103: исходное тело (уже содержит нормативную
матрицу, i18n-ключи и race-контракт от владельца), комментарий аналитики
2026-08-15 (`P3`, сложность 2/10, риск 3/10, «вопросов нет»), комментарий
автора ТЗ («продуктовых вопросов нет, метка остаётся `S3-spec` по поручению
владельца» — на момент ревью метка уже `S4-spec-review`, расхождение не
процессное: `docs/specs/README.md` подтверждает коммит и ветку).
2. Сверены обязательные разделы ТЗ (§7.1 PROCESS.md) построчно — таблица ниже.
3. Прочитан `src/device-toggle.ts` целиком: подтверждено существование
`ResolvedToggleIntent`, `ToggleNextEffect`, `formatToggleIntent()`,
`sameToggleOperationTargets()`, `toggleOperation()` — ровно тот резолвер
#94, на который ссылается ТЗ, а не придуманный интерфейс.
4. Прочитан `src/houseplan-card.ts` (окрестности `_clickDevice`, `:4336-4400`
и `:14838-14846`): подтверждено дословно, что текущий `_tapConfirm` —
`{ text: string; exec: () => void }`, а `text` строится только как
`this._t('confirm.tap_toggle', { name })` — то есть претензия ТЗ «диалог
показывает только имя» верна для текущего кода, не выдумана.
5. Прочитан `_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. Дублирования семантики без причины не
обнаружено.
6. Проверена логика `coverLikeService()` (`:307-331`) против нормативной
матрицы §6 ТЗ — см. Low-3.
7. Прочитан `test/device-toggle.test.mjs` и список `demo/smoke_*.mjs`:
подтверждено существование `smoke_ha_controls.mjs`, `smoke_controls.mjs`,
`smoke_virtual_light_toggle.mjs`, на которые ссылается план регрессии §11 —
не выдуманные имена.
8. Прочитан `docs/TOUCH-SUPPORT.md`: строка «View dialogs and safe device
actions» — «Fully supported» на touch (не best-effort), то есть touch-
влияние здесь блокирующее по DoR; ТЗ §8 адресует это узкой mobile-footer и
narrow-viewport smoke — соответствует канону.
9. Прочитан `docs/USER-GUIDE.ru.md:489` и «Краткая памятка безопасности»
(:1206): термин «Переключить состояние» и рекомендация включать
подтверждение для Toggle/Run/Cover совпадают с ТЗ дословно.
10. Проверена трассируемость: `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`),
существующий toast `toast.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; все три либо правятся косметически
при следующей редакции, либо снимаются этой записью без нового цикла.