mirror of
https://github.com/Matysh/houseplan-card
synced 2026-10-04 05:41:34 +00:00
223 lines
20 KiB
Markdown
223 lines
20 KiB
Markdown
# 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, а неточность формулировки внутри
|
||
текущего ТЗ — чинится правкой текста в этом же документе.
|