Files
houseplan-card/docs/reviews/SPEC-REVIEW-522-r1.md
T
2026-09-10 18:40:54 +00:00

230 lines
19 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-522-r1
## Якоря
- Issue: [#522](https://github.com/Matysh/houseplan-card/issues/522) «Клик по
маркеру устройства ставит курсор в поле имени: мигающая каретка и
всплывающая клавиатура на планшете», метка `S4-spec-review` (подтверждено
`gh issue view 522 --repo Matysh/houseplan-card --json labels`:
`bug`, `P3`, `S4-spec-review`).
- ТЗ: тело issue #522, раздел `## ТЗ` (строки 47–118 полученного тела,
`gh issue view 522 --repo Matysh/houseplan-card --json body -q .body`).
- sha256 нормализованного тела (как получено `gh issue view --json body -q
.body`, без дополнительной нормализации):
`f2efc9f21eed616183e678956a0bb1e273d36563dc2129216a1d2314bb890ffe`
- Заход r1. Бюджет циклов §4 до этого ревью: 0/4.
- Единственный комментарий issue — `S2-анализ` (Codex, 2026-09-10,
`dev d5575f63`), переводящий задачу на полный трек с названным нарушенным
критерием §5 («новый UX-контракт», «touch impact»). Продуктовых вопросов
владельцу в комментарии не задано.
## Скоуп ревью
Ревью ТЗ (PROCESS.md §2.4) для полного трека. Предмет — раздел `## ТЗ`:
контракт К1–К5, AC1–AC7, откат, release-артефакты — против §7.1
(обязательные разделы), §2.5 (DoR) и `docs/SCOPE.md`/`docs/TOUCH-SUPPORT.md`
(продуктовая рамка и touch-контракт).
## Как проверялось
1. Прочитано тело issue #522 целиком (разделы «Симптом», «Что происходит на
самом деле», «Причина», «Это не свежий регресс», «## ТЗ») и единственный
комментарий.
2. Сверено с `docs/SCOPE.md` (персона/поверхность, партиальный бэклог
«Touch ergonomics of the editors»), `PROCESS.md` §1, §2.3–2.5, §4, §5,
§7.1, `AGENTS.md` (разделы Specs, Gates).
3. Прочитан `docs/TOUCH-SUPPORT.md` целиком — safety floor и правило
deliberate degradation не нарушаются: изменение чинит поведение внутри уже
объявленного best-effort редакторского контура, View/kiosk не задеты.
4. Прочитан `src/hp-dialog.ts` целиком. Текущая логика `_focusInitial()`
(строки 396–404), `_focusableElements()` (347–394) и Tab-ловушка
`_onKeyDown` (461–492) дословно совпадает с тем, что описывает issue:
порядок `autofocus || focusable[0] || .close(native) || .surface ||
ha-dialog`, `.close` — вне `_focusableElements()` (рендерится в shadow
DOM хоста, а не в light DOM/slot), `.surface` уже несёт
`tabindex="-1"`.
5. `grep -n "autofocus" -r src/` — подтверждены все 6 фактических деклараций:
`hp-confirm.ts:54` (кнопка «Отмена», используется всеми диалогами
подтверждения), `houseplan-editor-runtime.ts:9826` (экспорт бэкапа,
кнопка), `:9966` (импорт бэкапа, кнопка), `:12351` (поиск в инбоксе,
поле, `type="search"`), `:5531` (текст декора, `textarea`),
`space-copy-runtime.ts:91` (имя копии пространства, поле) — сверено с
перечислением К3.
6. `grep -n "namein\|marker.name_label"` в `houseplan-editor-runtime.ts` —
подтверждена строка 12999–13000: первое поле диалога маркера —
`input.namein type="text"` без `autofocus`, точно как в «Причине» issue.
7. Прочитан `docs/USER-GUIDE.ru.md` вокруг строк 439–442 (фокус-ловушка во
всех диалогах — `Tab`/`Shift+Tab`/`Esc`/возврат фокуса — не меняется,
К4 это подтверждает) и 2198–2206 («Отмена» получает начальный фокус в
диалогах подтверждения удаления/разблокировки — К3 явно сохраняет
поведение диалогов с объявленным `autofocus`, эта строка гайда не
протухает).
8. Проверена реализуемость AC1–AC5 существующей техникой проекта:
`demo/smoke_help_affordance.mjs` (строки 1–29) уже создаёт голый
`<hp-dialog>`, наполняет его вручную и вызывает `dialog._focusInitial()`
напрямую — тот же приём закрывает AC3/AC5 без браузерного клика;
`test/hp-dialog-contract.test.mjs` — существующий файл именно с этим
именем, который ТЗ называет свидетелем AC3, подтверждён `ls test/`.
9. Прочитана шапка `scripts/mutation-gate.mjs` (комментарий-документация,
строки 1–29) — формат AC6 («мутант + названный гард-смок») соответствует
механике реестра и требованию `PROCESS.md` §2.7 про защитные AC на
дорогом гейте (smoke).
10. `grep` по `demo/smoke_*.mjs` и `test/*.test.mjs` на `.close`/`.surface`/
`_focusInitial` — ни один существующий тест не фиксирует конкретно путь
«фокусируемых элементов в light DOM вообще нет → `.close`» (нужен для
находки Low-2 ниже).
11. Сверено содержание ТЗ с чек-листом DoR §2.5 пункт за пунктом (см.
«Что проверено и корректно»).
12. `git status` — рабочая копия чистая; кода по этой задаче ещё нет (стадия
ТЗ, ветки не существует).
## Находки
### Low-1 — К3 называет неверное число диалогов с объявленным `autofocus`
**Файл:** тело issue #522, `## ТЗ` → `### Контракт`, К3: «Их сегодня
четыре: подтверждения (`hp-confirm.ts`), экспорт и импорт бэкапа — на
кнопке «Отмена»; поиск в инбоксе устройств, имя копии пространства, текст
декора — на поле.»
Перечисление называет шесть отдельных мест: `hp-confirm.ts` (общий
компонент, кнопка), экспорт бэкапа (кнопка), импорт бэкапа (кнопка), поиск
в инбоксе (поле), имя копии пространства (поле), текст декора (поле) — grep
по `src/` (шаг 5 выше) подтверждает ровно эти шесть строк с `autofocus`, а
не четыре. Даже если считать «экспорт и импорт» одной категорией, получится
пять, не четыре.
**Почему не блокирует.** Правило К3 сформулировано как общее утверждение
(«любой диалог, объявивший `autofocus`, не меняет поведения»), а не как
перечень с фиксированной длиной — механизм не зависит от числа. Ни один AC
не проверяет счётчик. Опечатка декоративная.
**Решение:** Low, waived. Для протокола — заменить «четыре» на «шесть» или
убрать число вовсе, оставив перечисление.
### Low-2 — К1 называет `.close`/`ha-dialog` «фолбэками без изменений», хотя их достижимость меняется
**Файл:** тело issue #522, `## ТЗ` → `### Контракт`, К1, пункт 4.
Сегодняшний порядок в коде (`src/hp-dialog.ts:399–402`): `autofocus →
focusable[0] → .close (только нативный диалог) → .surface → ha-dialog`.
Новый порядок по К1: `autofocus → первый нетекстовый → .surface → далее
существующие фолбэки без изменений (.close, ha-dialog)`. Дословно это
переставляет `.surface` **перед** `.close`, а не оставляет `.close` на его
сегодняшнем месте — контракт называет это «без изменений», но фактически
меняет порядок.
Единственный сценарий, где раньше побеждал `.close`, — нативный диалог, у
которого в light DOM нет вообще ни одного элемента, подходящего под селектор
`_focusableElements()` (не только текстовых — вообще никаких: ни кнопки, ни
чекбокса). В новом порядке этот же сценарий (шаг 2 не находит нетекстовый,
потому что массив пуст) отдаёт фокус `.surface`, а не `.close`, потому что
`.surface` рендерится безусловно в том же нативном branch и теперь стоит
раньше. Практическое отличие ничтожно: `.surface` — та же ловушка
(`tabindex="-1"`), `Tab` по-прежнему находит `.close` (он отдельно
подставляется первым элементом списка в `_onKeyDown`, строка 476), а такого
полностью пустого диалога в текущем инвентаре компонентов не нашлось (шаг
10 выше — ни один смок/юнит не фиксирует именно этот путь, значит он либо
не используется, либо не критичен). Контракт при этом полностью
детерминирован и реализуем буквально как написан — исполнителю нечего
угадывать.
**Решение:** Low, waived. Для протокола — точнее было бы «`.close` и
`ha-dialog` остаются в конце списка, но `.surface` теперь проверяется перед
ними», а не «без изменений».
## Что проверено и корректно
- **К1–К5 непротиворечивы и полностью детерминированы.** К2 закрывает
единственный реальный источник будущей двусмысленности — новые/неизвестные
HTML input-типы — явным правилом «список нетекстовый, а не наоборот»:
`search`, `tel`, `url`, `email`, `password`, `date`/`time`/`datetime-local`,
`number` и любой будущий тип автоматически считаются текстовыми без
правки правила.
- **AC1–AC7 пронумерованы, у каждого назван способ доказательства**
(`smoke`/`unit`/мутант/ревью кода) — требование DoR §2.5 выполнено
построчно.
- **AC6 самостоятельно закрывает требование §2.7** про обязательный мутант
для защитного AC, проверяемого дорогим гейтом (AC1/AC2 — `smoke`): автор
явно называет, какой мутант должен покраснеть на каком свидетеле, до
начала код-ревью, а не постфактум — соответствует уроку #435/#430,
явно процитированному в PROCESS.md.
- **Продуктовая рамка присутствует, хоть и не под отдельным заголовком.**
Персона (Home admin) и поверхность (редактор устройств, диалог маркера,
клик) читаются из разделов «Симптом»/«Что происходит на самом деле» —
они в том же теле issue, до заголовка `## ТЗ`. «Что человек увидит до/
после» отвечено одной фразой без терминов реализации: «мигающая каретка/
клавиатура» → «клавиатура больше не поднимается» (раздел «Touch»). То же
обращение с рамкой, разнесённой по телу issue, а не сведённой в один
подраздел `## ТЗ`, уже было принято в `SPEC-REVIEW-520-r1` — прецедент
того же формата «спецификация в теле issue».
- **Риск 1 (потеря мгновенного набора текста в ~7–8 существующих диалогах)
прицельно проверен как кандидат в продуктовый вопрос владельцу**
(«сколько видимых изменений входит в issue», §7.1) — решение прозрачно,
с названной ценой и причиной («это цена того, что тап по любому диалогу
перестаёт открывать клавиатуру»), не выдано за факт без обоснования. Это
разумный дефолт инженера, а не замаскированная догадка — не считаю
находкой.
- **К5 корректно ограничивает скоуп**: `autofocus` существующим диалогам не
добавляется в этой задаче, расширение — отдельный issue. Соответствует
правилу «скоуп не расширяется» (PROCESS.md §3.9).
- **i18n, миграция/совместимость, touch, откат, release-артефакты** —
присутствуют, ни один пункт не оставлен пустым без явного «нет» и
обоснования.
- **Выбор полного трека обоснован** названным нарушенным критерием §5
(«новый UX-контракт начального фокуса» + влияние на touch-контракт) —
сделано в S2-анализе, ТЗ ему не противоречит.
- **Ни одного вопроса владельцу не задано** — и после отдельной проверки
двух кандидатов (Риск 1 выше; сама природа изменения — правка одного
внутреннего механизма без новой видимой сущности) не нахожу нерешённого
продуктового вопроса, который стоило бы вынести. Единственный технический
спор, который я нашёл сам (Low-2), — технический по типу (что считать
«без изменений» в порядке фолбэков), не продуктовый, решается ревью, а не
владельцем.
- **«Принято предположительно» оформлено по формату §7.1** — все четыре
пункта помечены явно, ревьюер вправе их оспорить; ни один не выдаёт
догадку за факт.
## Чего не проверял
- **Гейты кода** (`tsc --noEmit`, `npm test`, `npm run build`,
`check-docs.mjs`) — не гонял: на стадии ТЗ нет ни строки правки в
`src/**`, ветки задачи не существует. Предмет этого этапа — текст ТЗ, не
код; гейты — предмет код-ревью после `S5-ready`.
- **`npm run invariants`** — не применимо: геометрия и ссылки на неё не
затронуты.
- **Регистрация мутанта AC6 в `scripts/mutation-gate.mjs`** — не может
существовать на этапе ТЗ (мутант патчит ещё не написанный код); проверка
— предмет код-ревью.
- **Реальный браузерный прогон смоков AC1/AC2/AC4/AC5** — они ещё не
написаны; оценивал реализуемость по прямому прецеденту
(`demo/smoke_help_affordance.mjs`), не запускал ничего нового.
- **Golden/скриншоты** — не проверял: ТЗ заявляет «визуально ничего не
меняется», согласуется с характером правки (перераспределение фокуса,
не разметки/стилей); отдельно не перепроверял рендер.
## Унаследовано из r<N-1>
Не применимо — первый раунд (r1) ревью ТЗ #522.
## Вердикт
Зелёный. High: 0, Medium: 0, Low: 2 (обе — неточности формулировок в К1 и
К3, не влияют на реализуемость, проверяемость AC или продуктовое поведение;
сняты решением ревьюера с записью выше, без правки текста ТЗ). Переход:
«Готово к разработке» (`S5-ready`).
---
<!-- material-anchors: сгенерировано конвейером (#414) -->
## Материал раунда
- Ветка: `dev`, коммит `b2346eaac32c` — ребейз его осиротит, и это нормально: ниже якоря, которые ребейз не меняет.
- Дерево материала: `94fcf05d432a71d43e1d7324aa3f670f7bc7b402`
```
git log --all --format='%H %T' | grep 94fcf05d432a
```
- Тело issue: `d7b95ee0bb522938c91da3c1a5b8fd3dc4bc5acde4fad93a04f7740178dee14f`
- Вердикт конвейера: `green` · High 0