# 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) уже создаёт голый ``, наполняет его вручную и вызывает `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 Не применимо — первый раунд (r1) ревью ТЗ #522. ## Вердикт Зелёный. High: 0, Medium: 0, Low: 2 (обе — неточности формулировок в К1 и К3, не влияют на реализуемость, проверяемость AC или продуктовое поведение; сняты решением ревьюера с записью выше, без правки текста ТЗ). Переход: «Готово к разработке» (`S5-ready`). --- ## Материал раунда - Ветка: `dev`, коммит `b2346eaac32c` — ребейз его осиротит, и это нормально: ниже якоря, которые ребейз не меняет. - Дерево материала: `94fcf05d432a71d43e1d7324aa3f670f7bc7b402` ``` git log --all --format='%H %T' | grep 94fcf05d432a ``` - Тело issue: `d7b95ee0bb522938c91da3c1a5b8fd3dc4bc5acde4fad93a04f7740178dee14f` - Вердикт конвейера: `green` · High 0