Files
houseplan-card/docs/reviews/SPEC-REVIEW-152-r1.md
T
2026-09-04 09:29:35 +03:00

25 KiB
Raw Blame History

Ревью ТЗ — issue #152, цикл r1

  • Issue: https://github.com/Matysh/houseplan-card/issues/152
  • ТЗ: docs/specs/152-room-click-fit.md, ветка issue/152-room-click-fit, коммит 7f8751b4dce84880edaab95600be7f028b9fab5f
  • Этап: spec (PROCESS.md §2.4)
  • Вердикт: красный · цикл r1/4 · High: 2 · Medium: 2

Скоуп ревью

Прочитаны в указанном порядке: docs/SCOPE.md, AGENTS.md, PROCESS.md (§1, §2, §3, §4, §7, §8, §12), тело issue #152 и все три комментария (уточнение и самоотзыв про double-tap на фоне, актуализация аналитики 2026-08-15, хендофф автора ТЗ), docs/USER-GUIDE.ru.md (терминология «Карточка комнаты», клик по комнате в Resize, пространство/комната), docs/UX-MODES.md, docs/TOUCH-SUPPORT.md, docs/CANVAS.md (§4–8: content frame, ZOOM_MIN/MAX, viewport/editor handoff), docs/CONFIG-COMPATIBILITY.md. Issue не помечен small/trivial (аналитика: сложность 4/10, риск 5/10, «обычный трек»), поэтому ТЗ корректно вынесено в отдельный файл docs/specs/152-room-click-fit.md, а не в тело issue. Формат ревью — полный документ.

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

ТЗ активно ссылается на «существующее» поведение как на данность (существующий resolver, существующий double-tap, существующая доступная подпись, существующие ZOOM_MIN/MAX). Каждое такое утверждение сверено с реальным кодом, а не принято на веру:

  • src/houseplan-card.ts:4792-4793, src/space-geometry.ts:203 — ZOOM_MIN/ ZOOM_MAX/MIN_ZOOM существуют, значения 8 и 1/3 подтверждены.
  • src/houseplan-card.ts:5023-5035 (_resetZoom) и :5264-5300 (_stagePointerUp) — double-tap-to-reset реализован, но целиком внутри if (this._kiosk) { … }; вне этого блока на stage нет ни @dblclick, ни иной обработки двойного тапа (проверено также по полному списку обработчиков стейджа, :14471-14480).
  • src/houseplan-card.ts:14619-14646 (room hover через нативные @mouseenter/@mouseleave на каждой SVG-форме комнаты) против :6528, :9936, :11045, :11096 ([...rooms].reverse().find(r => this._pointInRoom(raw, r)), используется инструментами Resize/Delete/Merge/Split) — это два разных механизма, не один вызываемый resolver.
  • src/houseplan-card.ts:16001-16065 (_renderRoomLabel) — .roomlabel не имеет tabindex/role/aria-label/@keydown; единственные tabindex в файле — tabindex="-1" на editor-тулбарах (:9241, :16963, :17044), подтверждает docs/SCOPE.md («no ARIA labelling of the plan»).
  • gh issue view 82 — открыт, всё ещё S3-spec: гипотеза «до реализации #82» в ТЗ корректна, ViewportAnimator в src/ действительно не найден (grep).
  • gh api repos/Matysh/houseplan-card/issues/28 — state: closed, state_reason: not_planned, закрыт 2026-08-14, то есть до публикации этого ТЗ.
  • docs/specs/146-four-phase-sun-background.md §15 и docs/specs/138-adjacent-room-autoclose.md §13 — эталон формата AC с инлайн-аннотацией способа доказательства, использован как мера сравнения.

Находки

High-1 — ни один AC не указывает способ доказательства

Где: docs/specs/152-room-click-fit.md §11 (строки 172–187).

Раздел перечисляет 14 пронумерованных критериев без пометки unit/backend/ smoke/golden/«ревью кода» у каждого — в отличие от §12 «План тестирования», который группирует проверки по категории, но не привязывает их к номерам AC. Например, AC5 («Flat и Isometric проверяются по итоговой экранной геометрии») может быть закрыт unit-тестом проекции, browser smoke со скриншотом или golden-эталоном — все три упомянуты в §12, и по тексту ТЗ невозможно понять, какой из них действительно обязателен для приёмки, а какой — просто сопутствующий.

PROCESS.md §2.5 (DoR) требует буквально: «AC1…ACn — пронумерованные проверяемые критерии приёмки; у каждого указано, чем он доказывается: unit / backend / smoke / golden / «ревью кода»». Соседние ТЗ этого же репозитория соблюдают это дословно (docs/specs/146-four-phase-sun-background.md:417-465: «AC1 (unit; разработчик):» и так для всех 16 пунктов; аналогично docs/specs/138-adjacent-room-autoclose.md:276-323). ТЗ #152 — единственное из свежей серии, где это правило не выполнено ни разу.

Почему High, а не Low: это не стилистическая придирка, а прямое условие готовности к разработке (§2.5) и то, что мне прямо поручено проверить в этом ревью («указание способа доказательства» для каждого AC). Без этой аннотации issue не может законно перейти в S5-ready.

Что нужно поправить: проставить у каждого из AC1–AC14 способ доказательства и исполнителя, как в §15 docs/specs/146-...md.

High-2 — потерян отдельный проверяемый AC про синхронность hit targets во время перехода

Где: docs/specs/152-room-click-fit.md §8 (строки 130–151) и §11 (172–187); ср. issue #152, пункт критериев приёмки «Viewport и все SVG/HTML hit targets остаются синхронны во время перехода» (14-й из 15 чекбоксов тела issue).

Это требование в финальном ТЗ не стало отдельным AC — оно осталось прозой внутри §8: «Viewport, SVG и HTML overlays получают один и тот же camera state на каждом кадре; промежуточное рассогласование hit targets недопустимо.» Ни одного номера в §11, ни отдельного пункта browser smoke в §12 (там есть «Flat/Isometric screenshot bounds; ни один видимый слой или hit target не отстаёт» — но это один общий пункт про финальный кадр, а не про сам переход кадр за кадром) под это не выделено.

Сценарий, которому это открывает дорогу: как только заработает ViewportAnimator (#82) и переход из клика по комнате в её fit станет анимированным tween'ом, визуальный viewBox обновляется на каждом кадре, а SVG/HTML hit-элементы (устройства, проёмы, room label) должны обновляться синхронно. Если реализация пересчитывает hit-геометрию на кадр позже видимой, пользователь тапает туда, где устройство видно сейчас, и промахивается — либо получает клик по случайно оказавшемуся там другому объекту. Именно этот класс дефекта («hit target отстаёт от кадра во время перехода») уже упомянут как прецедент в AGENTS.md (#89 — «смок, который не умел падать» стоил целого цикла ревью), и без отдельного AC с проверяемым доказательством ничто не гарантирует, что тест на это вообще напишут, а не просто положатся на прозу §8.

Почему High: это не новая функциональность сверх скоупа — она уже была явно сформулирована владельцем в теле issue как критерий приёмки и молча выпала при переносе в ТЗ. Восстановление контракта — не расширение задачи, а исправление ТЗ до его полноты.

Что нужно поправить: вернуть требование как отдельный пронумерованный AC с указанным способом доказательства (browser smoke с промежуточными кадрами tween, не только конечным) и явной строкой в §12.

Medium-1 — accessibility-раздел выдаёт «существующую accessible-подпись» за факт, хотя такой доступности сегодня нет

Где: docs/specs/152-room-click-fit.md §9 (строки 153–163), формулировка «Существующая доступная подпись либо room-card target получает action Enter и Space».

.roomlabel (src/houseplan-card.ts:16043-16056, _renderRoomLabel) сегодня не имеет ни tabindex, ни role, ни aria-label, ни @keydown — только pointer-обработчики для drag в Plan-редакторе и клик на дочерней ha-icon (переход в HA area). Единственные tabindex во всём файле — три tabindex="-1" на editor-тулбарах (:9241, :16963, :17044), что снимает tab- stop, а не добавляет. Это согласуется с прямой формулировкой docs/SCOPE.md («Accessibility: … no ARIA labelling of the plan») — сегодня в плане нет ни одного клавиатурно-доступного или ARIA-размеченного элемента вообще.

Формулировка §9 читается как «добавить действие к уже доступному элементу», а по факту задача вводит первый в продукте клавиатурно-фокусируемый, ARIA-размеченный элемент плана, причём по одному на каждую комнату — со всеми последствиями (порядок табуляции по потенциально многим комнатам, обнаруживаемость, стили :focus-visible, отсутствие конфликта с уже занятыми tabindex="-1" зонами тулбаров). Раздел §9 сам по себе технически осторожен (явно запрещает невидимую полноэкранную сетку tab-stop), но опирающееся на него предложение 1 и допущение из AC12 создают впечатление меньшего объёма работы, чем есть на самом деле, и не фиксируют явно, что это новая a11y-поверхность, а не расширение существующей.

Почему Medium: это не блокирует ни один AC — предложенное поведение реализуемо, задача была явно запрошена в теле issue (пункт про Enter/Space уже был в исходных критериях). Но формулировка занижает объём и скрывает продуктовый факт (первая ARIA-разметка плана), который стоит зафиксировать явно, а не как побочный эффект неточной фразы.

Заведён отдельный issue https://github.com/Matysh/houseplan-card/issues/182.

Medium-2 — «существующий» double-click/tap fit-all на фоне на деле существует только в kiosk

Где: docs/specs/152-room-click-fit.md §3 («не входит… изменение существующего double-click/tap fit всего плана на свободном фоне»), §7 («Double click/tap по свободному фону сохраняет существующий fit/reset всего плана без изменений»).

docs/UX-MODES.md документирует double-tap-reset только в разделе «Kiosk mode (v1.41.0)»: «double tap resets zoom». В разделе «View» такого жеста нет вовсе. Чтение кода подтверждает документацию: логика _lastTap/_resetZoom() в _stagePointerUp (src/houseplan-card.ts:5264-5277) целиком находится внутри if (this._kiosk) { … }; на самом stage нет @dblclick (полный список обработчиков — :14474-14480). То есть на обычном (не kiosk) View — ни десктопный dblclick мышью, ни touch double-tap — сегодня ничего не делают на фоне; жест существует только в kiosk.

ТЗ формулирует это как факт про View в целом, а не как допущение или явно ограниченное kiosk-наблюдение (в отличие от §16, где рядом стоящее допущение «до #82 переход атомарный» правильно вынесено в блок предположений). Первый комментарий владельца к issue («двойной клик/тап… – Поправка, это поведение кажется уже реализовано») показывает, что сама неоднозначность уже была на поверхности при заведении issue и не была закрыта явной проверкой.

Последствия: (1) пункт browser smoke «double tap свободного фона сохраняет прежнее поведение» (§12) содержателен только для kiosk — для обычного View «прежнее поведение» это просто отсутствие эффекта, что тривиально пройдёт даже при ошибке в общей gesture-арбитраже; (2) для ownership-логики §6–7 (отличить одиночный tap от первой половины double-tap) потребуется новая общая (не только kiosk) логика распознавания double-tap — её сегодня не существует, а формулировка «новые независимые константы не вводятся» (§7) намекает, что общий тайминг уже есть и его достаточно переиспользовать, что неверно вне kiosk.

Почему Medium: не блокирует ни один AC (инструкция «сохранить как есть» остаётся корректной независимо от истинного текущего охвата), но фактическая неточность способна привести к смоку, проверяющему не то окружение, и к недооценке объёма новой gesture-арбитражной логики.

Заведён отдельный issue https://github.com/Matysh/houseplan-card/issues/183.

Заведённые issue по Medium-находкам

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

  • Геометрический контракт (§4) — однозначен и самосогласован: safe rect 0.8W×0.8H, формула scale = min(0.8W/Rw, 0.8H/Rh), пример 2000×1000 → 800×800 с полями 100/100/600/600 px — арифметика проверена вручную и совпадает с телом issue дословно.
  • ZOOM_MIN/ZOOM_MAX действительно существуют (houseplan-card.ts:4792-4793, space-geometry.ts:203, значения 8 и 1/3) — ссылка на «существующие пределы zoom» не выдумана.
  • #82 корректно учтён как ещё не реализованный — issue #82 открыт, S3-spec; ViewportAnimator в src/ не найден (grep). Условная формулировка «до реализации #82 … атомарно» точна, а не догадка.
  • Конфликт с #28 (§10) — сам факт конфликта (обычный клик не может одновременно принадлежать двум действиям) реален, а решение (primary click закреплён за fit-to-room) — явное продуктовое решение, а не техническая подмена; см. также Low-1 про устаревший статус #28 ниже.
  • Модель данных и миграция — по существу закрыто корректно: room fit не меняет room/space/backend-схему, новых полей не появляется; сверено с docs/CONFIG-COMPATIBILITY.md (нет ни одной точки пересечения с текущим реестром совместимости). Разнесено по §8/§15 вместо отдельного раздела (см. Low-3), но по содержанию пробелов нет.
  • Non-scope (§3) согласован с non-scope тела issue и не оставляет скрытого расширения (открытие карточки, кнопка «назад», авто-смена пространства, свободное вращение изометрии, второй animator — всё явно исключено).
  • Блок предположений (§16) используется по назначению: пункты про базис измерения 10%, состав bounds, атомарность до #82 и передачу trigger карточки в #28 — это действительно технические/уточняющие решения, а не спрятанные продуктовые вопросы, которые следовало адресовать владельцу. Ревьюер оспаривает формулировку пункта про «единственный resolver» (см. Low-4) в рамках штатного технического спора автор/ревьюер, не как продуктовый вопрос.
  • i18n — конкретный текст на двух локалях дан («Вписать комнату Гостиная» / «Fit room Living room»); имена ключей не выбраны, но это техническая деталь именования, а не продуктовая неясность.

Low (снято с записью, не блокирует)

  • Low-1. §10 обсуждает #28 как открытую задачу, которой перед реализацией «нужно изменить UX». По факту #28 закрыт not_planned 2026-08-14 — до публикации этого ТЗ. Само продуктовое решение (primary click → fit-to-room) от этого не меняется, но формулировку стоит поправить в следующей редакции, чтобы не выглядела как отсылка к живой задаче.
  • Low-2. §1 не называет явно персону из таблицы docs/SCOPE.md (Home admin / Household members / Guests) — сценарий описан через поверхность (View, kiosk) и типичный триггер (большой/сложный план), персона угадывается, но не названа. Не мешает пониманию контракта.
  • Low-3. В отличие от docs/specs/146-...md/138-...md, в ТЗ #152 нет отдельного заголовка «Модель данных и миграция» — содержание корректно, но разнесено по §8 и §15. Рекомендация на будущее — для единообразия со смежными ТЗ этого репозитория.
  • Low-4. §16 полагается на «единственный existing room hover/hit resolver». По коду это два разных механизма: hover в View — нативные @mouseenter/@mouseleave на SVG-форме каждой комнаты (:14619-14646), а инструменты Resize/Delete/Merge/Split используют собственный [...rooms].reverse().find(pointInRoom), продублированный в 4 местах (:6528, :9936, :11045, :11096). Оба, вероятно, совпадают на практике (одинаковый z-порядок), но структурно это не один переиспользуемый resolver. Пункт уже явно оформлен как допущение в §16, поэтому не требует отдельного issue — фиксирую как технический спор для реализации: имеет смысл вынести один явный shared-хелпер, а не полагаться на совпадение двух независимых путей.

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

  • Реализацию — её нет, это ревью ТЗ, а не кода.
  • Golden/визуальные артефакты и производительность — на этапе ТЗ не требуются; §12 корректно откладывает их проверку до реализации/пре-релиза.
  • Backend/custom_components/houseplan/**/*.py — задача их не касается по заявленному scope, отдельно построчно не сверялось, так как заявлено «room fit не меняет model/backend», а docs/CONFIG-COMPATIBILITY.md не показал точек пересечения.
  • Полное соответствие проектируемого «одного focus target на комнату» практикам ARIA authoring patterns — это вопрос code review на этапе реализации, не ТЗ.
  • Не выполнялись гейты (typecheck/test/build) — на этом этапе нет кода для их запуска, только документ.

Вывод

Два блокирующих дефекта: (1) ни один из 14 AC не указывает способ доказательства, что напрямую противоречит DoR (PROCESS.md §2.5) и практике соседних ТЗ этого репозитория; (2) явный критерий приёмки issue про синхронность SVG/HTML hit targets с viewport во время перехода потерялся при переносе в ТЗ и не имеет ни номера AC, ни отдельной smoke-проверки. Оба дефекта устраняются правкой текста ТЗ, а не пересмотром продуктового решения. Геометрический контракт, non-scope, обработка конфликта с #28/#82 и модель данных проверены по коду и корректны.

Возврат в «ТЗ в работе» (S3-spec), цикл r1/4.