mirror of
https://github.com/Matysh/houseplan-card
synced 2026-10-01 04:09:17 +00:00
@@ -0,0 +1,222 @@
|
||||
# SPEC-REVIEW-402-r1
|
||||
|
||||
- Issue: https://github.com/Matysh/houseplan-card/issues/402
|
||||
- Артефакт ТЗ: `docs/specs/402-confirm-outside-main-branch.md` (полный трек, класс A)
|
||||
- Заход: r1 · лимит циклов ревью ТЗ для полного трека — 4 (§4), израсходовано 0
|
||||
- Ревьюер: Claude (роль «ревьюер ТЗ»), независимая сессия, без устных пояснений автора
|
||||
- База сравнения: HEAD `d94db87e` (коммит, добавивший спецификацию)
|
||||
|
||||
## Скоуп ревью
|
||||
|
||||
Первый раунд ревью ТЗ для #402: регресс подтверждения опасного действия
|
||||
(`hp-confirm`), которое рендерится только в финальной ветке `render()`
|
||||
(`src/houseplan-card.ts`) и поэтому недоступно/теряется в остальных ветках
|
||||
(онбординг «нет пространств», `fixed_floor` pending/invalid, `!space`).
|
||||
Задача полного трека (P1, bug, класс A) — файл ТЗ обязателен, что и сделано.
|
||||
|
||||
## Как проверялось
|
||||
|
||||
1. Прочитаны `docs/SCOPE.md`, `AGENTS.md`, `PROCESS.md` целиком (разделы §2.4,
|
||||
§2.5, §5, §7.1, §7.2), тело issue #402 и единственный комментарий (аналитика
|
||||
S2).
|
||||
2. Прочитан весь текст ТЗ `docs/specs/402-confirm-outside-main-branch.md`.
|
||||
3. Каждое фактическое утверждение ТЗ о коде сверено с текущим деревом на SHA
|
||||
`d94db87e`:
|
||||
- структура `render()` (`src/houseplan-card.ts:11165-11884`) — все семь
|
||||
ранних веток и их точки возврата;
|
||||
- `HpConfirmController` (`src/danger-confirm.ts`) — поведение `confirm` /
|
||||
`resolve` / `cancel`;
|
||||
- вызов `_confirmDanger` из `_deleteServerPlan`
|
||||
(`src/houseplan-onboarding-runtime.ts:218-224`, клик на `:273`);
|
||||
- все восемь call site'ов `_confirmDanger` (`houseplan-card.ts`,
|
||||
`houseplan-editor-runtime.ts` ×5, `houseplan-onboarding-runtime.ts` ×2);
|
||||
- расположение `_tapConfirm` (`:11852-11868`) и `_vacCalConfirm`
|
||||
(`:11802-11812`) относительно `_dangerConfirm` (`:11872-11878`);
|
||||
- импорт `hp-confirm` — проверено, что он не ленивый (`houseplan-card.ts:14`,
|
||||
верхнеуровневый `import`), что подтверждает заявление ТЗ об отсутствии
|
||||
влияния на бюджет (AC7);
|
||||
- использование `noChange` во всём `src/**` — подтверждено, что сентинел
|
||||
нигде не оборачивается в шаблон (`grep noChange`), это делает техническое
|
||||
утверждение границы ТЗ («noChange нельзя обернуть») фактом, а не догадкой.
|
||||
4. Проверено соответствие терминологии `docs/USER-GUIDE.ru.md` (раздел про
|
||||
общий диалог подтверждения) — видимых текстов ТЗ не меняет, конфликта нет.
|
||||
5. Проверено `docs/TOUCH-SUPPORT.md` и DoR-чек-лист §2.5 на предмет
|
||||
обязательного пункта про touch/kiosk — см. находку H1.
|
||||
6. Проверены обязательные разделы §7.1 — присутствуют все (сценарий · что
|
||||
человек увидит · проблема/контракт · скоуп/не-скоуп · UX · модель данных ·
|
||||
i18n · AC1-7 с доказательством · план автотестов · риски · откат ·
|
||||
release-артефакты).
|
||||
7. Гейты кода (`tsc`, `test`, `build`) не гонялись: на этапе ТЗ продуктового
|
||||
диффа ещё нет, гонять их не над чем (см. «Чего не проверял»).
|
||||
|
||||
## Находки
|
||||
|
||||
### H1 (High, блокирует). ТЗ не называет влияние на touch/kiosk — обязательный, объявленный блокирующим пункт DoR
|
||||
|
||||
**Где**: весь файл `docs/specs/402-confirm-outside-main-branch.md` — ни разу
|
||||
не упоминает touch, kiosk или `TOUCH-SUPPORT.md` (`grep -i touch|kiosk` по
|
||||
файлу — ноль совпадений).
|
||||
|
||||
**Почему это находка, а не формальность**. PROCESS.md §2.5 перечисляет пункты
|
||||
«Готово к разработке» и явно помечает один из них как блокирующий отдельно от
|
||||
остальных: «влияние на touch по `docs/TOUCH-SUPPORT.md` (**View и киоск —
|
||||
блокирующие**)», и там же: «Если хоть один пункт не выполнен — статус не
|
||||
«Готово к разработке»». `TOUCH-SUPPORT.md` со своей стороны требует того же
|
||||
прямым текстом: «New editor feature specifications … must state one of:
|
||||
`Touch editor: supported` / `best effort / intentionally degraded` / `not
|
||||
exposed`».
|
||||
|
||||
Затронутая этим ТЗ поверхность — не абстрактный служебный код: это ровно
|
||||
диалог `<hp-confirm>` в состоянии «нет пространств» (онбординг) и в
|
||||
`fixed_floor` pending/invalid, то есть экран, который согласно
|
||||
`TOUCH-SUPPORT.md` относится к категории «View dialogs and safe device
|
||||
actions» — «Fully supported» на touch, без исключений best-effort. Часть
|
||||
затронутых call site'ов (`houseplan-editor-runtime.ts`, 5 мест) при этом
|
||||
принадлежит редакторам, для которых допустима best-effort деградация — но
|
||||
только если она **явно объявлена**, а не подразумевается молчанием.
|
||||
|
||||
**Симптом отсутствия анализа**: без явного утверждения нельзя отличить
|
||||
«автор проверил — влияния нет» от «автор не думал про touch вовсе». Это
|
||||
особенно значимо здесь, потому что рефакторинг двигает `hp-confirm` из
|
||||
внутреннего дочернего узла одной `<ha-card>` в узел, рендерящийся отдельно от
|
||||
конкретной ветки разметки (см. Риск «двойной рендер диалога» в самом ТЗ) —
|
||||
у смены места крепления DOM-узла есть техническая возможность задеть
|
||||
touch-специфичные вещи (portal/DOM-scope для `dialog.showModal()`,
|
||||
`pointer-events`, `:focus-trap`), даже если по факту не заденет.
|
||||
|
||||
**Фактическая оценка (для экономии цикла)**: содержательно последствий,
|
||||
скорее всего, нет — `<hp-confirm>` остаётся тем же кастомным элементом с тем
|
||||
же shadow DOM и той же логикой модального диалога независимо от того, чьим
|
||||
прямым потомком в основном дереве он является; ни один AC1-7 не описывает
|
||||
изменение верстки, жестов или поведения `hp-confirm` самого по себе. Но это
|
||||
вывод ревьюера, а не факт, зафиксированный автором в ТЗ — а фиксировать его
|
||||
обязан автор (правило #163 «New editor feature specifications … must state
|
||||
one of»).
|
||||
|
||||
**Требуемая правка**: добавить в ТЗ явную строку по образцу
|
||||
`Touch: View/kiosk — fully supported, без изменений (тот же <hp-confirm>,
|
||||
маршрут рендера не меняет разметку/жесты диалога)`, и отдельно —
|
||||
`Touch editor: supported` для пяти call site'ов из `houseplan-editor-runtime.ts`,
|
||||
если они действительно не деградируют. Это правка одной-двух строк текста, не
|
||||
кода.
|
||||
|
||||
### M1 (Medium, в скоупе задачи). «Не-скоуп»-обоснование для `_tapConfirm`/`_vacCalConfirm` содержит фактическую неточность
|
||||
|
||||
**Где**: `docs/specs/402-confirm-outside-main-branch.md`, раздел «Скоуп /
|
||||
не-скоуп»: «Не в скоупе: … `_tapConfirm` и `_vacCalConfirm` — у них своя
|
||||
механика и свои ветки.»
|
||||
|
||||
**Проверено чтением кода**: `_tapConfirm` рендерится в
|
||||
`src/houseplan-card.ts:11852-11868`, `_vacCalConfirm` — в `:11802-11812`,
|
||||
`_dangerConfirm` — в `:11872-11878`. Все три блока лежат в **одной и той же**
|
||||
финальной ветке `render()` (последний `<ha-card>`, тот же самый шаблон, между
|
||||
ними нет ни одного `return`). Утверждение «свои ветки» в буквальном
|
||||
прочтении неверно: у них нет собственных веток `render()` — они делят ровно
|
||||
ту же ветку, что и `_dangerConfirm` до этого фикса, и подвержены тому же
|
||||
классу дефекта («открытое подтверждение исчезает при смене ветки», вторая
|
||||
половина дефекта из аналитики S2), например если многоклиентская
|
||||
синхронизация (J6, `docs/SCOPE.md`) уводит карточку в `fixed_floor` pending
|
||||
или обнуляет `model.length` **пока** открыт `_tapConfirm`/`_vacCalConfirm`.
|
||||
|
||||
Отличие от `_dangerConfirm`, которое у ревьюера НЕ вызывает вопросов и,
|
||||
похоже, и есть настоящая причина исключения: `_tapConfirm`/`_vacCalConfirm`
|
||||
не используют `HpConfirmController` и не отдают вызывающему `Promise` —
|
||||
`exec()` вызывается синхронно по клику, поэтому «вечно висящего промиса» у
|
||||
них в принципе не бывает (при потере диалога они не блокируют await
|
||||
вызывающего, а просто становятся недоступны до следующего рендера основной
|
||||
ветки). А сами точки входа (`marker.tap_confirm`, календарь пылесоса)
|
||||
физически недостижимы из веток онбординга/`fixed_floor`/`!space` — там нет
|
||||
устройств на плане, значит буквальный сценарий issue их не касается.
|
||||
|
||||
Это делает исключение из скоупа **разумным по существу**, но
|
||||
**обоснование в тексте — неверным**: «свои ветки» вместо настоящей причины
|
||||
(«синхронный exec без промиса» + «недостижимость точки входа из веток вне
|
||||
основной»). Ложное обоснование опасно не абстрактно: тот, кто будет
|
||||
реализовывать вынос `hp-confirm` из финальной ветки, должен точно знать, что
|
||||
`_tapConfirm`/`_vacCalConfirm` физически стоят в той же ветке и их нельзя
|
||||
случайно утащить вместе с `_dangerConfirm` при выносе блока — а формулировка
|
||||
«у них свои ветки» наводит на обратное представление.
|
||||
|
||||
**Требуемая правка**: заменить формулировку на техническую причину исключения
|
||||
(синхронный `exec`, недостижимость входа) и явно предупредить реализацию не
|
||||
трогать расположение этих двух блоков при выносе `_dangerConfirm`. Опционально
|
||||
(не обязательно для этого issue) — упомянуть остаточный риск «уже открытый
|
||||
`_tapConfirm`/`_vacCalConfirm` теряется при смене ветки во время
|
||||
многоклиентской синхронизации» как кандидата в отдельный issue, если владелец
|
||||
сочтёт его достаточно вероятным; в скоупе #402 фиксировать не обязательно —
|
||||
issue именно про мёртвую кнопку и висящий промис, которых у этих двух объектов
|
||||
нет.
|
||||
|
||||
## Что проверено и признано корректным
|
||||
|
||||
- **Диагноз дефекта** — точен и воспроизводим по строкам: `render()` действительно
|
||||
возвращает `hp-confirm` только в финальной ветке (`:11872`), все более ранние
|
||||
`return` (`:11166`, `:11170`, `:11171`, `fixed_floor` pending/invalid,
|
||||
`!model.length`, `!space`) реально существуют и реально не содержат
|
||||
`hp-confirm`.
|
||||
- **`HpConfirmController` описан верно**: `cancel()` действительно резолвит
|
||||
`false` через `resolve()`, `resolve()` сверяет токен, новый `confirm()`
|
||||
отменяет предыдущий запрос (`src/danger-confirm.ts:47-72`) — контроллер не
|
||||
участвует в дефекте, претензия ТЗ обоснована.
|
||||
- **Буквальный сценарий issue** (`_deleteServerPlan` из онбординга) сверен
|
||||
построчно: `src/houseplan-onboarding-runtime.ts:218-224` вызывает
|
||||
`this.host._confirmDanger`, кнопка в `:273` — реальная причина «мёртвой
|
||||
корзины» из issue.
|
||||
- **Техническое утверждение о `noChange`** («нельзя обернуть в шаблон») —
|
||||
подтверждено: во всём `src/**` `noChange` возвращается только как значение
|
||||
`render()` целиком (`houseplan-card.ts`, `editor.ts`, `space-card.ts`,
|
||||
`space-editor.ts`), ни разу не встречается внутри `${…}` — граница ТЗ не
|
||||
придумана, а списана с уже действующего паттерна кодовой базы.
|
||||
- **AC1-AC4, AC6** — проверяемы, у каждого назван способ доказательства
|
||||
(смок), и способ реалистичен: `demo/smoke_danger_confirmation.mjs` уже
|
||||
существует и уже покрывает контракт диалога, дополнение под новые ветки —
|
||||
органичное расширение того же файла.
|
||||
- **AC5** (иммедиат-отказ в `!_config||!hass` и `warm`) — продуман
|
||||
корректно: `!hass`/`!_config` не могут стать `false→true→false` в течение
|
||||
жизни примонтированной карточки (Lovelace не сбрасывает `hass`/`config` в
|
||||
falsy после первичной установки), то есть эта ветка достижима только до
|
||||
первого рендера — отказывать там немедленно безопасно и не теряет открытый
|
||||
диалог (терять нечего). Ветка `warm` реально достижима в течение сессии
|
||||
(смена языка), и там `noChange` **сохраняет** уже показанный диалог
|
||||
нетронутым (Lit не трогает DOM) — то есть AC5 верно ограничивает
|
||||
немедленный отказ только НОВЫМИ запросами, не описывая (и не должен
|
||||
описывать) отмену уже открытых.
|
||||
- **AC7 (бюджет)** — `hp-confirm` импортируется в шапке файла обычным
|
||||
(не ленивым) `import` (`houseplan-card.ts:14`), поэтому перемещение точки
|
||||
рендера не добавляет новый чанк/зависимость — заявление «правка структурная,
|
||||
бюджет не растёт» подтверждается фактическим импортом.
|
||||
- **Скоуп/не-скоуп в остальном** — граница с #32 (содержимое диалога,
|
||||
ревалидация после `await`) и с #406 (`alertdialog`/`aria-describedby`)
|
||||
названа явно и не пересекается с контрактом этого ТЗ.
|
||||
- **Обязательные разделы §7.1** — все присутствуют, включая «откат» и
|
||||
«release-артефакты» (changelog RU+EN).
|
||||
- **Соответствие `docs/SCOPE.md`**: чинит J4 (онбординг, «zero to a working
|
||||
plan») и снимает риск для J6 (multi-client sync может вызвать смену ветки
|
||||
под открытым диалогом) — задача не расширяет продукт, а восстанавливает
|
||||
ранее рабочее поведение (регресс против до-#32).
|
||||
- Метки issue (`bug`, `P1`, `S4-spec-review`, без `small`/`trivial`)
|
||||
согласуются с заявленным в ТЗ полным треком.
|
||||
|
||||
## Чего не проверял
|
||||
|
||||
- **Гейты кода** (`npx tsc --noEmit`, `npm test`, `npm run build`,
|
||||
`check-docs`, смоки, `bundle:budget`) — не гонялись: на этапе ТЗ
|
||||
продуктового кода ещё нет, диффа для гейтов не существует. Это будет
|
||||
предметом код-ревью после реализации.
|
||||
- **`scripts/mutation-gate.mjs` / `demo/smoke_danger_confirmation.mjs`** —
|
||||
не запускал, только убедился, что оба файла существуют и их формат
|
||||
(реестр мутантов, browser-smoke на реальной карточке) совместим с планом
|
||||
автотестов ТЗ.
|
||||
- **Таблицу `docs/specs/README.md`** — строка для #402 в неё не добавлена,
|
||||
но это не регрессия этого ТЗ: специфика #395-#401 туда тоже не занесена
|
||||
(таблица не поддерживается систематически, известный долг §7.3 п.1
|
||||
документа процесса), поэтому не поднимаю отдельной находкой.
|
||||
- **Реальный рендер в браузере** — на этапе ТЗ кода нет, воспроизведение
|
||||
дефекта в описании ТЗ и issue взято на веру как «воспроизведено исполнением»
|
||||
автором аналитики; независимо не перепроверял (это будет предметом
|
||||
смок-доказательства в код-ревью).
|
||||
- **`_tapConfirm`/`_vacCalConfirm` как отдельный дефект** — не завожу
|
||||
отдельный issue: по результату разбора (см. M1) это не тот же класс
|
||||
дефекта (нет висящего промиса, нет недостижимости из веток issue), поэтому
|
||||
это не «Medium вне скоупа» по #202, а неточность формулировки внутри
|
||||
текущего ТЗ — чинится правкой текста в этом же документе.
|
||||
Reference in New Issue
Block a user