Files
houseplan-card/docs/reviews/SPEC-REVIEW-565-r1.md
T
2026-09-14 07:06:16 +00:00

16 KiB
Raw Blame History

SPEC-REVIEW — issue #565, заход r1

Вердикт: зелёный · заход r1 · блокирующих циклов 0/4 · High: 0 · Medium: 0

Скоуп

ТЗ в теле issue #565 (комментарии добавляют только аналитику и Q&A-цикл владельца; финальный текст ТЗ — раздел ## ТЗ тела issue, дополненный решениями владельца от 2026-09-14). Три независимых дефекта доступности основного View и статической houseplan-space-card, найденные аудитом #560:

  1. полоса вкладок пространств/этажей не сообщает программно, какая вкладка открыта (нет aria-current/роли навигации);
  2. доступное имя устройства при тревоге дублирует одно и то же локализованное слово («Alarm, Alarm») — сборка сегментов независимо повторена в основной карточке и в houseplan-space-card;
  3. device-tooltip показывается только через pointer-события (mouse-hover gate), клавиатурный фокус кольцо рисует, а подсказку — нет.

Явно вне скоупа: контраст подписей комнат (F2) — решение владельца оставить как есть; общий редизайн доступности, ARIA tablist/roving tabindex, touch/pen tooltip, интерактивность houseplan-space-card, изменение визуала устройств. Данных/миграций/конфига задача не касается.

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

  • Прочитано тело issue #565 и все комментарии (gh issue view 565) — аналитика владельца, вопросы Q1–Q3 и финальные решения владельца от 2026-09-14.
  • Прочитаны docs/SCOPE.md (J1/J2/J3, персоны, View как продукт для household/kiosk), PROCESS.md §2.4, §2.5, §7.1 (обязательные разделы ТЗ, формат вердикта, лимит циклов), docs/TOUCH-SUPPORT.md (раздел «Pointer modality and hover ownership»), docs/UX-MODES.md (show_room_tooltip), docs/USER-GUIDE.ru.md (терминология «пространство/этаж», «вкладка», «подсказка», настройка «Цвет границ и названий»).
  • Все четыре фактических утверждения ТЗ о ТЕКУЩЕМ поведении сверены с кодом на SHA 60f93a10 напрямую и через подчинённого агента (grep + чтение файлов, без исполнения):
    • src/houseplan-card.ts:11469–11493 — полоса вкладок: обычный <div class="tabs">, активность только через class="active", нет aria-current/aria-selected/aria-pressed/роли навигации. Рядом же в файле (aria-pressed на projection-toggle, строка ~11529) виден паттерн ARIA, применённый к другому контролу, но не к вкладкам — подтверждает, что дефект реален, а не выдуман.
    • src/houseplan-card.ts:12519–12533 (основной View) и src/space-render.ts:593–605 (houseplan-space-card, вызывается из src/space-card.ts) — обе версии независимо строят [name, state_a11y_*, pulse_a11y_*, …].filter(Boolean).join(', ') без дедупликации. deviceA11yState() (src/device-presentation.ts:67) и resolveDevicePulse() (src/device-pulse.ts:88–93) одновременно дают state==='alarm' и pulse.reason==='alarm' при тревоге — то есть дублирование гарантировано состоянием, а не подгонкой примера. marker.state_a11y_alarm/marker.pulse_a11y_alarm действительно идентичны по тексту в en/ru/de/fr (src/i18n/*.json:241,251); полный паритет ключей en/ru/de/fr подтверждён (по 1362 ключа в каждом файле, без пропусков).
    • src/houseplan-card.ts:12534–12573 — маркер устройства: role="button" tabindex="0", обработчики @click/@keydown/@contextmenu/@pointerover/ @pointerleave/@pointerdown/@pointermove/…, но ни @focus, ни @blur нет. _showDeviceTip(:5735) → _showTip(:7427) гейтятся через this._pointerModality.hoverEnabled, а оно в src/pointer-modality.ts:30–32 истинно только при modality==='mouse' && hoverCapable — mouse-only гейт подтверждён. Визуальное кольцо фокуса подтверждено отдельно: src/styles/devices.styles.ts:416–419, .dev:focus-visible.
    • Общей pure-функции дедупликации сегментов в src/*.ts не найдено — её придётся создавать с нуля, как и предполагает ТЗ.
    • Аналог решения для «фокус открывает то же, что и hover» уже есть в кодовой базе: src/hp-help.ts:227–284 — _scheduleOpen/_scheduleClose/ _triggerFocus/_triggerBlur проверяют trigger?.matches(':focus-visible') для tooltip настроек. Это подтверждает, что предложенный в разделе «Принято предположительно» способ (:focus-visible в обработчике focus, без глобального modality-стора) реалистичен и уже опробован в проекте, а не является непроверенной догадкой.
    • demo/smoke_household_journeys.mjs (323 строки) существует, использует page.evaluate на window.__card; инфраструктура пригодна для расширения под AC1/AC2/AC5/AC6/AC7, как и заявляет план автотестов. Существующий J5 уже сверяет активную вкладку с текущим space — годная опора для AC1/AC2.
    • Инструменты, названные в плане автотестов (scripts/no-new-any.mjs, scripts/smoke-select.mjs), существуют в scripts/.
    • Настройка space.room_color = «Цвет границ и названий» подтверждена в src/i18n/ru.json:411 — ТЗ корректно НЕ переименовывает её (Q1 owner reversed: в итоговом решении вариант с раздельным цветом текста и переименованием отклонён, значит space.room_color остаётся как есть).
  • Дельта/наследование из r0 не применимо — документов предыдущих раундов нет (docs/reviews/ не содержит файлов по #565), это первый заход.

Находки

Блокирующих (High) и Medium в скоупе нет. Две низкие заметки, обе сняты без возврата автору:

L1 (Low, снято). Раздел «Скоуп»/АС1/АС2 называют вкладки «этаж»/«полоса этажей», хотя канонический термин по docs/USER-GUIDE.ru.md — «пространство» (этаж — лишь один из видов пространства наряду с двором, гаражом, отдельным строением, строка 61). Раздел i18n корректно хеджирует «навигация по пространствам/этажам», то есть автор осведомлён о нюансе. Снимаю: код и сама документация тоже свободно используют «этаж» как разговорный синоним (docs/USER-GUIDE.ru.md:470-490), AC не завязаны на конкретный текст строки объявления, а только на наличие aria-current и именованной области — точный текст остаётся инженерным решением по умолчанию.

L2 (Low, снято). Контракт tooltip клавиatурного фокуса перечисляет конкретные триггеры очистки (blur, смена пространства, вход в редактор, исчезновение маркера), но не называет явно «реальное touch/pen-событие на устройстве с уже открытым focus-tooltip» (гибридное устройство: клавиатура + сенсорный экран). Снимаю: docs/TOUCH-SUPPORT.md уже фиксирует общий инвариант «touch/pen немедленно очищают transient hover, включая tooltip», а раздел «Принято предположительно» прямо разрешает разделяемый lifecycle/clamp между pointer- и focus-tooltip, «если он не ломает существующий hover-контракт» — то есть наследование этого инварианта уже предусмотрено, отдельного пункта не требуется.

Наблюдение не для этого документа, а для код-ревью (не находка ТЗ). Существующий demo/smoke_household_journeys.mjs уже содержит проверку j2.alarm_not_colour_only (label.split(',').length >= 2, строка ~95) — такая проверка не отличит «два разных сегмента» от «дублированного сегмента» и не докажет AC3 сама по себе. АС3 сформулирован точно («слово тревоги ровно один раз»), так что это не дефект ТЗ, но код-ревьюеру стоит убедиться, что новый unit/smoke реально считает вхождения слова, а не полагается на старую проверку количества сегментов.

Что проверено и корректно

  • Обязательные разделы §7.1 все присутствуют: сценарий, что человек увидит до/после, проблема, скоуп/не-скоуп, контракт поведения, UX, модель данных/миграция, i18n, AC1–AC9 с доказательствами, план автотестов, риски, откат, release-артефакты.
  • Сценарий и «что человек увидит» описывают персону из docs/SCOPE.md (household/admin на десктопе, мышь/клавиатура/скринридер) и закрывают J1–J3 (обзор дома, тревога, безопасное действие) — не изобретают новую продуктовую рамку.
  • Все 4 фактических утверждения о текущем поведении подтверждены чтением кода (см. выше) — ни одна «догадка» не выдана за факт.
  • AC1–AC9 однозначны, каждый называет способ доказательства (browser smoke / unit / source-contract / regression); ни один не противоречит не-скоупу (AC8 явно защищает F2 от случайной правки).
  • «Принято предположительно» содержит только инженерные детали (имя ключа, расположение pure-функции, механизм детекции fokus-visible, общий clamp) и не прячет продуктовое решение — все три вопроса владельцу (Q1–Q3) заданы и закрыты явными решениями, размытых мест не осталось.
  • i18n-план не трогает существующие state_a11y_*/pulse_a11y_* и правильно требует паритета en/ru/de/fr для нового ключа — подтверждено, что паритет всех словарей уже полный (1362/1362), так что регресса паритета из-за добавления одного ключа не предвидится при обычной дисциплине.
  • «Откат», «модель данных и миграция», «AC9» корректно утверждают отсутствие миграции/схемы — согласуется с реальной природой правки (представление и сборка доступных имён, без нового состояния).
  • Release-артефакты требуют changelog RU+EN в одном пользовательском коммите и явно запрещают упоминать несуществующее изменение контраста — соответствует решению владельца по F2.

Чего не проверял

  • Не запускал npx tsc --noEmit / npm test / npm run build / браузерные smoke — предмет ревью ТЗ это не код (кода по issue ещё нет), а проверяемость и однозначность спецификации; фактическое поведение на этом SHA сверено чтением существующих файлов, а не выполнением.
  • Не проводил собственный контраст-аудит F2 — он explicitly вне скоупа задачи, решение владельца зафиксировано.
  • Не проверял полный screen-reader прогон (NVDA/VoiceOver) — вне скоупа ТЗ («не проводить общий редизайн доступности... полный screen-reader аудит»).
  • Не проверял все 1362 ключа en/ru/de/fr на смысловую точность перевода — только структурный паритет (количество и совпадение имён ключей), этого достаточно для цели проверки (нет орфанных ключей до добавления нового).

Материал раунда

  • Ветка: issue/565-view-accessibility, коммит 60f93a10ed5d — ребейз его осиротит, и это нормально: ниже якоря, которые ребейз не меняет.
  • Дерево материала: 4de72d37f155947c97e60beb944af00a2039021a
    git log --all --format='%H %T' | grep 4de72d37f155
    
  • Тело issue: 86508a821b1cdb4b2b7ad0b6e79395cb2e8ff094ea42f52487221af5c5e28093
  • Вердикт конвейера: green · High 0