diff --git a/docs/reviews/SPEC-REVIEW-152-r2.md b/docs/reviews/SPEC-REVIEW-152-r2.md new file mode 100644 index 00000000..70ac7bde --- /dev/null +++ b/docs/reviews/SPEC-REVIEW-152-r2.md @@ -0,0 +1,230 @@ +# Ревью ТЗ — 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 + ```