# Ревью ТЗ — issue #152, заход r2 - **Issue:** https://github.com/Matysh/houseplan-card/issues/152 - **ТЗ:** `docs/specs/152-room-click-fit.md`, ветка `issue/152-room-click-fit`, коммит `f672de67` (родитель ревьюируемой правки — `f29c4c5b`) - **Этап:** spec (PROCESS.md §2.4) - **Вердикт:** зелёный · заход r2 · блокирующих циклов 1/4 · High: 0 · Medium: 0 ## Материал раунда Вердикт r1 (комментарий issue) называет коммит `0fd4e66`, документ `docs/reviews/SPEC-REVIEW-152-r1.md` в заголовке — коммит `7f8751b4dce8…`. Ни один из двух SHA не резолвится в текущей истории (`git cat-file -t` на оба — `fatal`). Per PROCESS.md §2.10 это «обычное дело», а не находка сама по себе: ветку между раундами перебазируют/сквошат. Материал восстановлен по содержимому, а не по SHA: `docs/reviews/SPEC-REVIEW-152-r1.md` цитирует ТЗ построчно (§8 130–151, §9 153–163, §11 172–187, пример 2000×1000→800×800, формулировки «существующая accessible-подпись», «существующий double-click/tap fit-all»), и коммит `f29c4c5b` («docs: specify room click fit», Aug 15, начало истории файла) даёт файл, в котором все эти цитаты находятся на совпадающих строках дословно. Это и есть материал r1: он же единственный родитель следующего коммита `f672de67`, поэтому дельта раунда — `git diff f29c4c5b..f672de67 -- docs/specs/152-room-click-fit.md`. Два SHA, названных r1 в разных местах, не резолвятся оба — вероятно, r1 работал на ветке, где рабочий коммит амендился/ребейзился уже после того, как были сняты оба значения, и они не сверялись друг с другом перед публикацией (§7.2 требует сверки на момент вывода). Отмечаю это как процессное наблюдение уровня Low применительно к прошлому раунду; на выводы текущего раунда это не влияет, поскольку материал восстановлен по содержимому надёжно. ## Скоуп ревью Дельта — не локальная правка: `git diff --stat` даёт 487 добавленных / 252 удалённых строк на файле, который в исходной редакции насчитывал 270 строк, то есть фактически полная переработка документа (изменился даже заголовок: «Issue #152 — …» → «ТЗ #152 — …», добавлен раздел «Подтверждённое текущее состояние», переписаны структура и формулировки всех разделов). Per PROCESS.md §2.10 («разбор остаётся полным, если … объём дельты сопоставим с исходной задачей») разбор проведён полностью, а не только по находкам r1. Прочитаны: `docs/SCOPE.md` (Core user jobs, персоны, lock invariant), AGENTS.md/PROCESS.md (§2.4, §2.5, §2.7, §2.10, §3, §4, §7.1, §7.2), тело issue #152 и все три комментария, `docs/reviews/SPEC-REVIEW-152-r1.md`, текущий `docs/specs/152-room-click-fit.md` целиком, `docs/SCOPE.md` (персоны и accessibility-рамка), исходный код (`src/houseplan-card.ts`, `src/viewport-transition.ts`) для проверки каждого утверждения раздела «Подтверждённое текущее состояние» и математики геометрического контракта, а также текущий статус issue #28, #82, #182, #183 через `gh issue view`. ## Как проверялось ТЗ активно опирается на раздел «Подтверждённое текущее состояние» — каждое его утверждение сверено с `origin/dev` заново, а не принято на веру (код с момента r1 успел измениться: #82 реализован): - **#82 реализован.** `gh issue view 82` → `state: CLOSED, stateReason: COMPLETED`. В коде есть `CameraTransitionController` (`src/viewport-transition.ts:119`), `CameraTransitionReason = 'button' | 'wheel' | 'fit' | 'home' | 'double-tap'` (`:10`), инстанс в `houseplan-card.ts:1241`, `CAMERA_FIT_MS = 220` (`:438`). Утверждение ТЗ «единый camera transition, реализован» — факт, не догадка. - **Room hover сменил механизм со времён r1.** r1 цитировал `mouseenter`/`mouseleave` на SVG-форме; в текущем коде этих обработчиков нет вовсе (`grep` — 0 совпадений), room floor теперь вешает `@pointerenter` / `@pointerleave` (`houseplan-card.ts:11700-11731`, функция `enterRoom` пишет `_hoverRoom`). Формулировка ТЗ «browser target … определяют тот же room hit, что текущий hover» верна для **текущего** кода, а не устарела — обновление корректно отражает факт, что #82 заодно перевёл hover на pointer-события. - **Kiosk-only double-tap-reset подтверждён на актуальном коде.** `_stagePointerUp` (`:6970-6982`) — весь блок `_lastTap`/`_resetZoom` внутри `if (this._kiosk)`; вне kiosk на stage нет ни одного обработчика double-tap. Совпадает с утверждением ТЗ п.4 «Подтверждённого текущего состояния» и с находкой r1 Medium-2. - **`.roomlabel` не клавиатурно-доступна.** `_renderRoomLabel` (`:12756-12805`) не содержит `role`/`tabindex`/`aria-label`/`keydown`. Совпадает с утверждением ТЗ п.2 и с находкой r1 Medium-1 — в новой редакции claim сужен до `.roomlabel`, а не до «плана в целом» (важно, см. ниже). - **Пустое имя не рендерит подпись в View.** `_renderRoomLabel:12761`: `if (!r.name && !this._markup) return nothing;` — подтверждает claim § «Клавиатура и доступность» про edge case пустого имени. - **`LS_ZOOM` хранит только zoom.** `_saveZoom` (`:6735-6746`) пишет `this._zoomBySpace` (объект `{spaceId: zoom}`), без центра камеры — подтверждает п.5 «Подтверждённого текущего состояния». - **`ZOOM_MIN`/`ZOOM_MAX`** не изменились (`8` и `1/3`, `houseplan-card.ts:6448-6449`, `space-geometry.ts:245`). - **Архитектурная возможность AC15 (room-reason не пишет zoom-only state) подтверждена чтением, не только заявлена.** `_settleCameraTransition` (`:1277-1284`) сегодня безусловно вызывает `_saveZoom()` для любого `reason`, но `state.reason` уже прокинут через `CameraTransitionState` (`viewport-transition.ts:21`) в оба хука (`frame`/`settled`) — то есть ветвление по `reason === 'room'` не требует переделки контроллера, только добавления условия в существующий callback. ТЗ не выдаёт эту часть за уже решённую (это AC15 к разработке), но математика того, что она реализуема без второго controller-а, проверена. - **Интерактивные владельцы существуют как классы/элементы**, на которые ТЗ ссылается: `.dev`, `.oplock`, `.op-hit`, `.roomlabel` уже участвуют в других exclusion-списках (`:6794`, `:6815`), `data-hp="opening"` (`:13055`). - **Инструменты, упомянутые в плане тестов и release-артефактах, существуют:** `scripts/mutation-gate.mjs`, `scripts/smoke-select.mjs`, `demo/smoke_smooth_zoom.mjs`, `npm run bundle:budget`, `golden:verify`, `golden:accept`, `docs:accept`, `invariants` — все есть в `package.json`/дереве. `demo/smoke_smooth_zoom.mjs` уже содержит паттерн опроса промежуточных кадров через `requestAnimationFrame` в `page.evaluate` (`settle()`, строки 18-24) — ровно то, что требует AC10; проверяемость не гипотетична. - **#28 закрыт `NOT_PLANNED`** (`gh issue view 28`) — подтверждает и обновлённую формулировку ТЗ («#28 закрыта как not planned»), и закрытие Low-1 из r1. ## Закрытие раунда r1 | Находка r1 | Чем закрыта | Где это видно | |---|---|---| | **High-1** — ни один из AC1–AC14 не указывал способ доказательства | Раздел «Acceptance criteria и доказательства» переписан: 16 AC, у каждого инлайн-аннотация метода(ов) через `unit`/`smoke`/`golden`/`performance`/«ревью кода» и исполнителя (`Codex` либо `Codex/Claude`) | `docs/specs/152-room-click-fit.md:300-416`, AC1…AC16 | | **High-2** — критерий issue «hit targets синхронны во время перехода» не стал отдельным AC | Добавлен отдельный **AC10** с явной проверкой промежуточных кадров tween (не только финального), способ доказательства `smoke`, названа мутация | `:369-375` | | **Medium-1** (#182) — §9 выдавал `.roomlabel` за «существующую accessible-подпись» | Раздел «Подтверждённое текущее состояние» п.2 прямо констатирует отсутствие `role`/`tabindex`/`aria-label`/`keydown`; раздел «Клавиатура и доступность» явно назван «узким исключением из non-scope accessibility плана», а не расширением существующего. Claim сужен до `.roomlabel`, что закрывает и уточнение из закрытия #182 (про бейдж осиротевшего проёма — см. «Унаследовано» ниже) | `:34-35`, `:227-244` | | **Medium-2** (#183) — §3/§7 трактовали double-tap fit-all на фоне как поведение всего View | П.4 «Подтверждённого текущего состояния» и раздел «Double-tap без задержки single tap» ограничивают double-tap-reset kiosk’ом явно; non-scope добавляет отдельный пункт «добавление fit-all double-tap в обычный non-kiosk View»; выбран архитектурный путь, не требующий выноса 350-мс порога из kiosk-ветки (room tap принимается сразу, без окна ожидания) | `:39-40`, `:174-184`, `:67` | | Low-1 — §10 называл #28 живой задачей | Раздел «Связанные задачи» указывает «#28 (карточка комнаты закрыта как not planned)» | `:6-7` | | Low-2 — не названа персона из `docs/SCOPE.md` | «Сценарий»: «Житель либо home admin …» | `:11` | | Low-3 — нет отдельного заголовка «Модель данных и миграция» | Отдельный раздел «Данные, миграция, compatibility и i18n» | `:253-266` | | Low-4 — «единственный resolver» технически не один механизм (View hover vs. editor `pointInRoom`) | ТЗ больше не заявляет объединение с editor-resolver: click-ownership описан только для View/kiosk через тот же SVG pointer-target, что и hover (`data-hp="room"` + `@pointerenter`); редакторы в non-scope и не затрагиваются | `:146-154`, «Скоуп»: «работает только в основном View, включая kiosk» | ## Унаследовано из r1 Наследуется без повторной проверки на этом заходе (дельта их не задевает, подтверждено чтением текущей редакции ТЗ и кода выше, где это пересекалось): - Геометрическая арифметика примера 2000×1000 → 800×800 (проверена вручную в r1; в r2 формула и пример перенесены дословно, `:137-138`, дополнительно ре-проверены построчно в этом заходе как часть полного разбора). - Конфликт с #28 и решение в пользу primary-click за fit-to-room — продуктовое решение не пересматривалось, только формулировка статуса #28 (см. таблицу выше). - Non-scope не создаёт скрытого расширения задачи — подтверждено заново в этом заходе, поскольку раздел был существенно переписан (не наследование, а повторная проверка из-за нелокальности дельты). - Источник: `docs/reviews/SPEC-REVIEW-152-r1.md`, материал — коммит `f29c4c5b` (см. «Материал раунда» выше). ## Находки Блокирующих (High) и Medium-находок в этом заходе нет. **Low-note (не блокирует, к сведению).** Закрывающий комментарий #182 (2026-08-23) уточняет, что на `origin/dev` уже существует один клавиатурно/ARIA-доступный элемент — бейдж осиротевшего проёма (`role="button" tabindex="0" aria-label"`, `houseplan-card.ts:18806`), то есть план сегодня не абсолютно лишён доступной разметки. Текущая редакция ТЗ этой ошибки не повторяет: она нигде не утверждает «в плане нет ни одного доступного элемента» — claim сужен до `.roomlabel` конкретно, что фактически верно независимо от бейджа проёма. Считаю снятым без правки текста. ## Что проверено и корректно - **Обязательные разделы §7.1** присутствуют: сценарий + «что человек увидит до/после» первыми, скоуп/не-скоуп, геометрический и pointer/touch контракт, camera/intent/persistence, клавиатура и доступность, данные/миграция/i18n, AC1–AC16 с доказательством и исполнителем, план автотестов, риски, откат, release-артефакты — все на месте и в этом порядке. - **Блок «Принятые предположения»** (8 пунктов) использован по назначению: каждый пункт либо прямая цитата уже принятого владельцем решения из тела issue (10%-база, LS_ZOOM session-only, отсутствие невидимого tab-stop, статус #28), либо честно техническое решение реализации (single-tap без 350 мс окна, переиспользование `CAMERA_FIT_MS`, scope wall body по room provenance, SVG target как authority без нового resolver) — открытых продуктовых вопросов, замаскированных под предположения, не найдено. - **DoR-требования (§2.5), проверяемые на этапе ТЗ**: i18n-ключи названы (`room.fit_action`, en+ru), миграция/compatibility решены явно («не меняются», сверено с `docs/CONFIG-COMPATIBILITY.md` по неизменности схемы), производительность/бюджеты названы отдельным разделом, touch — блокирующая поверхность по `docs/TOUCH-SUPPORT.md`, откат описан («revert frontend/tests/docs/bundle», без обратной миграции), открытых продуктовых вопросов нет. - **Все факты раздела «Подтверждённое текущее состояние» и математика геометрического контракта** — перепроверены по актуальному `origin/dev` (список — в «Как проверялось»), расхождений с кодом не найдено. - **Non-scope согласован** с non-scope тела issue и не создаёт скрытого расширения: отдельно исключены Labs-флаг/новый config, floor-wide tab-stop grid, второй animator, вращение изометрии, general non-kiosk double-tap. - **Персона (`docs/SCOPE.md`)** названа явно («Житель либо home admin»), задача закрывает пространственную навигацию View (не отдельный Core user job, а улучшение J1 «show the whole home» applied к одной комнате) — в рамках guard rail SCOPE.md, не excess-функциональность. ## Чего не проверял - Дешёвые гейты (`npx tsc --noEmit`, `npm test`, `npm run build`) не запускал: дельта раунда — исключительно `docs/specs/152-room-click-fit.md` (`git show --stat f672de67` — один файл), кода это ревью не касается, а предыдущий прогон этих гейтов не может отражать документную правку. На этапе «ТЗ на ревью» кода к тестированию ещё нет (то же ограничение отмечал r1). `node scripts/check-docs.mjs` не запускал по той же причине: диап не трогает `src/**`. - Golden/browser smoke/performance/invariants — не запускал: реализации нет, §12 ТЗ корректно откладывает их до этапа «В разработке»/пре-релиза. - Backend/`custom_components/houseplan/**/*.py` — задача их не касается по заявленному и подтверждённому non-scope (модель/backend не меняются). - Полную ARIA-authoring-экспертизу «один action target на подпись» — вопрос код-ревью на этапе реализации, не ТЗ. - Обоснованность продуктового решения «primary click = fit-to-room» как такового — оно принято владельцем в теле issue и не пересматривается ни в этом, ни в прошлом заходе. ## Вывод Оба High из r1 закрыты предметно (полная таблица доказательств AC1–AC16 + восстановленный AC10 про синхронность hit targets на промежуточных кадрах), оба вынесенных Medium (#182, #183) отражены в тексте точнее, чем было — и подтверждены их же закрывающими комментариями от 2026-08-23 как полностью влитые в #152. Полный разбор, оправданный нелокальностью дельты (документ переписан почти полностью), не нашёл новых High/Medium: геометрический контракт, pointer/touch ownership, camera/intent lifecycle, accessibility и release-артефакты проверены по актуальному коду (включая то, что успело поменяться из-за реализации #82) и корректны. ТЗ готово к переводу в «Готово к разработке». --- ## Материал раунда - Ветка: `issue/152-room-click-fit`, коммит `f672de674fcf` — ребейз его осиротит, и это нормально: ниже якоря, которые ребейз не меняет. - Дерево материала: `862dfc7f9622c8432d7dc288446dc360a4900ea2` ``` git log --all --format='%H %T' | grep 862dfc7f9622 ``` - ТЗ `docs/specs/152-room-click-fit.md`, блоб `fb036d2bb9cc522555c55e15000d7d693e204f8b` ``` git log --all --find-object=fb036d2bb9cc522555c55e15000d7d693e204f8b -- docs/specs/152-room-click-fit.md ```