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

19 KiB
Raw Blame History

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-контракт).

Как проверялось

  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

Не применимо — первый раунд (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