19 KiB
SPEC-REVIEW-522-r1
Якоря
- Issue: #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-контракт).
Как проверялось
- Прочитано тело issue #522 целиком (разделы «Симптом», «Что происходит на самом деле», «Причина», «Это не свежий регресс», «## ТЗ») и единственный комментарий.
- Сверено с
docs/SCOPE.md(персона/поверхность, партиальный бэклог «Touch ergonomics of the editors»),PROCESS.md§1, §2.3–2.5, §4, §5, §7.1,AGENTS.md(разделы Specs, Gates). - Прочитан
docs/TOUCH-SUPPORT.mdцеликом — safety floor и правило deliberate degradation не нарушаются: изменение чинит поведение внутри уже объявленного best-effort редакторского контура, View/kiosk не задеты. - Прочитан
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". 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.grep -n "namein\|marker.name_label"вhouseplan-editor-runtime.ts— подтверждена строка 12999–13000: первое поле диалога маркера —input.namein type="text"безautofocus, точно как в «Причине» issue.- Прочитан
docs/USER-GUIDE.ru.mdвокруг строк 439–442 (фокус-ловушка во всех диалогах —Tab/Shift+Tab/Esc/возврат фокуса — не меняется, К4 это подтверждает) и 2198–2206 («Отмена» получает начальный фокус в диалогах подтверждения удаления/разблокировки — К3 явно сохраняет поведение диалогов с объявленнымautofocus, эта строка гайда не протухает). - Проверена реализуемость AC1–AC5 существующей техникой проекта:
demo/smoke_help_affordance.mjs(строки 1–29) уже создаёт голый<hp-dialog>, наполняет его вручную и вызываетdialog._focusInitial()напрямую — тот же приём закрывает AC3/AC5 без браузерного клика;test/hp-dialog-contract.test.mjs— существующий файл именно с этим именем, который ТЗ называет свидетелем AC3, подтверждёнls test/. - Прочитана шапка
scripts/mutation-gate.mjs(комментарий-документация, строки 1–29) — формат AC6 («мутант + названный гард-смок») соответствует механике реестра и требованиюPROCESS.md§2.7 про защитные AC на дорогом гейте (smoke). grepпоdemo/smoke_*.mjsиtest/*.test.mjsна.close/.surface/_focusInitial— ни один существующий тест не фиксирует конкретно путь «фокусируемых элементов в light DOM вообще нет →.close» (нужен для находки Low-2 ниже).- Сверено содержание ТЗ с чек-листом DoR §2.5 пункт за пунктом (см. «Что проверено и корректно»).
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— ребейз его осиротит, и это нормально: ниже якоря, которые ребейз не меняет. - Дерево материала:
94fcf05d432a71d43e1d7324aa3f670f7bc7b402git log --all --format='%H %T' | grep 94fcf05d432a - Тело issue:
d7b95ee0bb522938c91da3c1a5b8fd3dc4bc5acde4fad93a04f7740178dee14f - Вердикт конвейера:
green· High 0