Files
houseplan-card/docs/reviews/SPEC-REVIEW-152-r2.md
T
2026-09-04 06:53:57 +00:00

231 lines
22 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Ревью ТЗ — 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) и корректны. ТЗ готово к переводу в «Готово к
разработке».
---
<!-- material-anchors: сгенерировано конвейером (#414) -->
## Материал раунда
- Ветка: `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
```