mirror of
https://github.com/Matysh/houseplan-card
synced 2026-09-30 19:58:50 +00:00
@@ -0,0 +1,342 @@
|
||||
# CODE-REVIEW-152-r1
|
||||
|
||||
- Issue: https://github.com/Matysh/houseplan-card/issues/152
|
||||
- Ветка: `issue/152-room-click-fit`
|
||||
- SHA материала: `a54addb694a38baad7fe289c7eb9ab3d90c99704`
|
||||
- Диапазон: `origin/dev..HEAD` (merge-base `d5bfa0e3`)
|
||||
- Заход: r1 (код-ревью) · блокирующих циклов израсходовано 0 из 4 на этом этапе
|
||||
- ТЗ: `docs/specs/152-room-click-fit.md` (r2, зелёное ревью ТЗ подтверждено в
|
||||
`docs/reviews/SPEC-REVIEW-152-r2.md`, коммит `f672de67`)
|
||||
|
||||
## Скоуп
|
||||
|
||||
10 коммитов от `f29c4c5b` (спека) до `a54addb6` (принятие provenance
|
||||
скриншотов). Продуктовый код: новый `src/room-fit.ts` (чистая геометрия/hit
|
||||
ownership), интеграция в `src/houseplan-card.ts` (`_stagePointerDown/Move/Up/Cancel`,
|
||||
`_fitRoom`, `_roomFitBounds`, `_renderRoomLabel`, resize-refit), новый
|
||||
`CameraTransitionReason: 'room'` в `src/viewport-transition.ts`, механический
|
||||
вынос части существующих хелперов в новый `src/card-runtime.ts` (лимит размера
|
||||
основного файла), 4 новых мутанта в `scripts/mutation-gate.mjs`, поднятие
|
||||
`bundle:budget` потолка 294 000 → 296 000 Б (документировано), i18n en/ru/de/fr,
|
||||
`demo/smoke_room_fit.mjs`, `test/room-fit.test.mjs`, документация
|
||||
(CANVAS/ISOMETRIC/TESTING/USER-GUIDE/CHANGELOG×2).
|
||||
|
||||
Трейлеры: все коммиты несут `Issue: #152`; `User-Visible: yes` только на
|
||||
`4aad9fac` (фича) — тот же коммит правит `docs/CHANGELOG.md` и
|
||||
`docs/CHANGELOG.ru.md`. Остальные коммиты — `User-Visible: no` (тесты,
|
||||
документация, фикс без пользовательского эффекта). Соответствует правилу.
|
||||
|
||||
## Как проверялось
|
||||
|
||||
| Гейт | Команда | Результат |
|
||||
|---|---|---|
|
||||
| typecheck | `npx tsc --noEmit` | зелёный, без вывода |
|
||||
| unit | `npm test` | 1897 tests · 1896 pass · 0 fail · 1 skip (совпадает с заявлением автора) |
|
||||
| build | `npm run build` | зелёный, `dist` пересобран |
|
||||
| bundle sync | `npm run bundle:sync` | зелёный; `git status` после пересборки чист — три копии бандла побайтно совпадают с коммитом |
|
||||
| bundle budget | `npm run bundle:budget` | зелёный: initial View 294 862 Б при потолке 296 000 (±2000); предупреждение о запасе 5 138 Б — это раскрытый и обоснованный в самом диффе долг #367, не новая находка |
|
||||
| docs fingerprint | `node scripts/check-docs.mjs` | зелёный (diff трогает `src/**`, гейт обязателен) — «7 files, 12 external links» |
|
||||
| mutation-gate AC6 | `node scripts/mutation-gate.mjs --id=room-fit-interactive-owner-leaks-through` | мутант поймал: `smoke_room_fit.mjs` краснеет |
|
||||
| mutation-gate AC7 | `node scripts/mutation-gate.mjs --id=room-fit-pan-release-reaccepted` | мутант поймал |
|
||||
| mutation-gate AC8 | `node scripts/mutation-gate.mjs --id=room-fit-enters-kiosk-double-tap-sequence` | мутант поймал |
|
||||
| mutation-gate AC10 | `node scripts/mutation-gate.mjs --id=room-fit-html-overlay-jumps-ahead` | мутант поймал |
|
||||
| smoke (AC-named) | `node demo/smoke_room_fit.mjs` | зелёный, все 14 свидетелей `true` |
|
||||
| smoke (AC9) | `node demo/smoke_smooth_zoom.mjs` | зелёный |
|
||||
| smoke (smoke-select, сильная связь на затронутый `_stagePointerCancel`) | `node demo/smoke_editor_gestures.mjs` | зелёный |
|
||||
| smoke (smoke-select, сильная связь на `_holdFired/_panStart/_pinchStart/_swipeStart`) | `node demo/smoke_long_press_gesture.mjs` | зелёный |
|
||||
| smoke (smoke-select, прямое совпадение `_applyView/_baseVb/_clampView/_panStart/_stageEl/_view`) | `node demo/smoke_pan_any_zoom.mjs` | **КРАСНЫЙ — необработанное исключение, см. High-1** |
|
||||
| smoke (smoke-select, прямое совпадение) | `node demo/smoke_infinite_canvas.mjs` | зелёный |
|
||||
| smoke (smoke-select, прямое совпадение) | `node demo/smoke_kiosk_pan_lock.mjs` | зелёный |
|
||||
| smoke (smoke-select, прямое совпадение) | `node demo/smoke_decor.mjs` | зелёный |
|
||||
| smoke (smoke-select, прямое совпадение) | `node demo/smoke_furniture.mjs` | зелёный |
|
||||
| smoke (та же семья вызова `_stagePointerUp` плоским объектом) | `node demo/smoke_backdrop.mjs`, `node demo/smoke_hide_layers.mjs` | зелёные — крашится только View-ветка `smoke_pan_any_zoom` |
|
||||
| ручная мутация AC15 (нет в реестре, см. Medium-2) | правка `houseplan-card.ts`, `bundle:sync`, `node demo/smoke_room_fit.mjs`, откат | `roomIntentIsSessionOnly` перевернулся в `false` — защита реальна, но не зарегистрирована |
|
||||
| ручная проверка AC14 в редакторах (см. Medium-1) | ad-hoc браузерный скрипт через `demo/serve.mjs`, `_mode='devices'`/`'decor'` | `role="button" tabindex="0" aria-label="Fit room Office"` присутствуют, Enter/Space не производят эффекта |
|
||||
| golden | не запускал | AC3/AC4 сами называют метод `unit + golden`, но план тестирования спеки прямо откладывает golden на пре-бету (`docs/specs/152-room-click-fit.md` §«План тестирования»: «Golden, полный smoke и performance — перед beta»), это же в PROCESS.md §8. Не расхождение, а согласованное разделение обязанностей |
|
||||
| backend (`pytest tests_backend`) | не запускал | diff не трогает `custom_components/**/*.py` |
|
||||
| invariants (`npm run invariants`) | не запускал | diff не меняет модель стен/комнат/layout/marker.space/open_spans — правится только камера |
|
||||
| полный `demo/smoke_*.mjs` (220 файлов) | не запускал | непропорционально: диапазон правит camera/pointer-подсистему View, а не «всё»; выбор по `smoke-select.mjs` плюс прямые попадания выше признан достаточным, кроме описанной ниже находки |
|
||||
| performance_smoke | не запускал | не названо в AC, perf-бюджет описан только как «не регрессирует» и закрыт бандл-бюджетом + отсутствием новых DOM-чтений (см. код-чтение AC16) |
|
||||
|
||||
`node scripts/smoke-select.mjs --base origin/dev --head HEAD` вывод: 5 изменённых
|
||||
файлов `src/**`, 101 символ на изменённых строках, порог «широкого» символа — 44
|
||||
смока. 59 файлов — «прямое совпадение», ядро (`smoke_room_fit`,
|
||||
`smoke_smooth_zoom`, `smoke_editor_gestures`, `smoke_long_press_gesture`,
|
||||
`smoke_pan_any_zoom`, `smoke_infinite_canvas`, `smoke_kiosk_pan_lock`,
|
||||
`smoke_decor`, `smoke_furniture`) прогнан выше; остальные — совпадения по
|
||||
одному распространённому символу (`_mode`, `_baseVb`, `_view`, `_zoom`,
|
||||
`stopPropagation`, `_applyView`), которые сами инструмент и правило ревью
|
||||
относят к «слабой связи» («повод посмотреть, а не обязанность прогонять»); я
|
||||
их не прогонял. Полная матрица (220 файлов) — обязанность пре-релиза
|
||||
(PROCESS.md §8), не этого цикла.
|
||||
|
||||
## Находки
|
||||
|
||||
### High-1. `demo/smoke_pan_any_zoom.mjs` падает необработанным исключением — новый код не переживает существующий вызов `_stagePointerUp`
|
||||
|
||||
**Файл:** `src/houseplan-card.ts:6220-6223` (`_roomOwner`), вызов из
|
||||
`_stagePointerUp` (`src/houseplan-card.ts:6941` и далее, `this._roomOwner(ev)`
|
||||
на аргументе `acceptedRoomFitGesture`).
|
||||
|
||||
**Воспроизведение:** на SHA `a54addb6`, чистое дерево, свежий `bundle:sync`:
|
||||
|
||||
```
|
||||
$ node demo/smoke_pan_any_zoom.mjs
|
||||
...
|
||||
page.evaluate: TypeError: e.composedPath is not a function
|
||||
at yg._roomOwner (…/houseplan-card-CAJGhl9a.js:4951:48660)
|
||||
at yg._stagePointerUp (…/houseplan-card-CAJGhl9a.js:4959:10851)
|
||||
at pu (eval at evaluate…)
|
||||
at drag (eval at evaluate…)
|
||||
at async inMode (eval at evaluate…)
|
||||
Node.js v22.23.2
|
||||
```
|
||||
|
||||
Скрипт вызывает `_stagePointerUp({ pointerId, clientX, clientY })` —
|
||||
лёгкий объект вместо настоящего `PointerEvent` — паттерн, которым в этом же
|
||||
демо-наборе пользуются ещё три смока (`smoke_backdrop.mjs`,
|
||||
`smoke_editor_gestures.mjs`, `smoke_hide_layers.mjs`); все три остаются
|
||||
зелёными, потому что вызывают `_stagePointerUp` только вне `_mode==='view'`,
|
||||
где `_roomOwner` рано возвращает `null` и до `ev.composedPath()` дело не
|
||||
доходит. `smoke_pan_any_zoom.mjs` — единственный, кто гоняет ровно этот сценарий
|
||||
(drag по пустому месту) в `_mode==='view'` (`inMode('view', …)` — первый вызов
|
||||
в скрипте), и там `_roomOwner` пытается прочитать `ev.composedPath()`
|
||||
безусловно.
|
||||
|
||||
**Причина:** `_stagePointerUp` теперь вычисляет `this._roomOwner(ev)`
|
||||
безоговорочно (как аргумент `acceptedRoomFitGesture`), даже когда
|
||||
`this._roomPointer` уже `null` и результат гарантированно будет отброшен
|
||||
`acceptedRoomFitGesture`'s `if (!candidate || …) return null;`. JS вычисляет
|
||||
аргументы до вызова функции, так что защита внутри `acceptedRoomFitGesture` не
|
||||
спасает — `ev.composedPath()` читается раньше.
|
||||
|
||||
**В проде не воспроизводится** — единственный продуктовый вызов
|
||||
`_stagePointerUp` идёт из `@pointerup=${(e: PointerEvent) => this._stagePointerUp(e)}`
|
||||
(`houseplan-card.ts:11556`) с настоящим DOM-событием, у которого
|
||||
`composedPath()` есть всегда. Но это не снимает находку: `demo/smoke_pan_any_zoom.mjs`
|
||||
входит в обязательный пре-релизный набор (PROCESS.md §8, `smoke` — гейт
|
||||
`validate.yml`) и **сейчас красный на этом SHA**, а не только «не прогнан
|
||||
автором» — это уже сломанный существующий тест, а не пропущенный.
|
||||
|
||||
**Почему High, а не Medium:** это не гипотетический край, а воспроизводимый
|
||||
крах существующего, ранее зелёного гейта, обязательного перед бетой; ревью не
|
||||
может подтвердить «оно работает», когда прогон падает необработанным
|
||||
исключением, а не assertion-ошибкой.
|
||||
|
||||
**Предполагаемое направление фикса** (не мой мандат — решает автор): либо
|
||||
защитить `_roomOwner`/`roomFitOwnerFromPath` от отсутствия `composedPath`,
|
||||
либо не звать `this._roomOwner(ev)` вообще, когда `this._roomPointer` уже
|
||||
`null` (это заодно и дешевле — не ходит по `composedPath()` на каждый
|
||||
posterup, у которого не было room-кандидата).
|
||||
|
||||
### Medium-1 (в скоупе). Подпись комнаты в редакторах Devices/Decor получает нерабочую клавиатурную кнопку — нарушает собственный AC14
|
||||
|
||||
**Файл:** `src/houseplan-card.ts:12798-12804` (`_renderRoomLabel`): `role`,
|
||||
`tabindex`, `aria-label` и `@keydown` гейтятся условием `!this._markup`, а
|
||||
`_markup` (`houseplan-card.ts:1707`) — это `this._mode === 'plan'`, то есть
|
||||
**только** редактор Plan. В Devices (`_mode==='devices'`) и Decor
|
||||
(`_mode==='decor'`) те же атрибуты присутствуют.
|
||||
|
||||
**Воспроизведение** (ad-hoc браузерный скрипт через `demo/serve.mjs`, комната
|
||||
с именем и `disp.showNames: true`):
|
||||
|
||||
```
|
||||
c._mode = 'devices'; c.requestUpdate(); await c.updateComplete;
|
||||
// .roomlabel[data-id="room-a"]:
|
||||
// role="button" tabindex="0" aria-label="Fit room Office"
|
||||
label.focus();
|
||||
label.dispatchEvent(new KeyboardEvent('keydown', { key: ' ', … }));
|
||||
// → c._view не изменился, c._roomFocus остался null
|
||||
|
||||
c._mode = 'decor'; // тот же результат: role="button" tabindex="0" aria-label="Fit room Office"
|
||||
```
|
||||
|
||||
`_roomLabelKey` (`houseplan-card.ts:7032`) начинается с
|
||||
`if (this._mode !== 'view' || …) return;`, так что Enter/Space в Devices/Decor
|
||||
не делают ничего — но screen reader и клавиатурный пользователь видят
|
||||
фокусируемую кнопку «Вписать комнату Office», которая молча ничего не
|
||||
исполняет.
|
||||
|
||||
**Нарушенный AC:** AC14 прямым текстом: «…editors не получают room action».
|
||||
Раздел «Touch, темы и режимы» ТЗ называет явно Plan/Backdrop, но общий принцип
|
||||
раздела «Клавиатура и доступность» — target есть только «в View/kiosk» — тоже
|
||||
нарушен для Devices/Decor.
|
||||
|
||||
**Почему Medium, не High:** не ломает основной сценарий (клик/тап в View),
|
||||
не искажает данные, задевает только клавиатурных/screen-reader пользователей
|
||||
внутри admin-only редакторов. Фикс тривиален и в скоупе: заменить
|
||||
`!this._markup` на `this._mode === 'view'` в атрибутах `role`/`tabindex`/
|
||||
`aria-label` и в условии `@keydown` подписи.
|
||||
|
||||
### Medium-2 (в скоупе). Защита AC15 (запрет `_saveZoom` для reason `room`) не зарегистрирована в mutation-gate, хотя доказывается только дорогим смоком
|
||||
|
||||
**Файлы:** `src/houseplan-card.ts:1122` (`if (state.reason !== 'room') this._saveZoom();`)
|
||||
и `:1142` (`if (presentedZoom !== undefined && reason !== 'room') this._saveZoom();`).
|
||||
|
||||
Спека сама формулирует AC15 так: «Mutation, направляющая reason `room` в
|
||||
`_saveZoom`, краснит storage spy» — то есть обещает мутанта. План тестирования
|
||||
относит AC15 к «дешёвым pure unit», которые ревьюер проверяет локальным снятием
|
||||
защиты. По факту защита доказана только внутри `demo/smoke_room_fit.mjs`
|
||||
(`roomIntentIsSessionOnly`) — не pure-unit тестом (`test/viewport-transition.test.mjs`
|
||||
не тронут вообще этим диффом) — то есть именно «дорогим гейтом (смок)», для
|
||||
которого PROCESS.md §2.7 требует обязательного мутанта в
|
||||
`scripts/mutation-gate.mjs`: «там ревьюер не воспроизведёт отрицательный прогон
|
||||
второй раз». Среди 4 новых мутантов диффа (`room-fit-interactive-owner-leaks-through`,
|
||||
`room-fit-pan-release-reaccepted`, `room-fit-enters-kiosk-double-tap-sequence`,
|
||||
`room-fit-html-overlay-jumps-ahead` — все покрывают AC6/AC7/AC8/AC10) мутанта на
|
||||
AC15 нет.
|
||||
|
||||
**Я проверил вручную, что защита реальна** (иначе это была бы находка High —
|
||||
непроверяемый AC): временно убрал оба условия `reason !== 'room'`,
|
||||
`bundle:sync`, `node demo/smoke_room_fit.mjs`:
|
||||
|
||||
```
|
||||
"roomIntentIsSessionOnly": false // было true
|
||||
FAILED (1): - roomIntentIsSessionOnly: expected true, got false
|
||||
```
|
||||
|
||||
Откатил правку, `bundle:sync`, дерево чисто на `a54addb6`. Защита работает
|
||||
сейчас — но без записи в реестре она не переживёт следующую правку камеры:
|
||||
будущий ревьюер (или CI) не узнает, что нужно её защищать, пока баг снова не
|
||||
доедет до пользователя (ровно сценарий, который описывает сам mutation-gate.mjs
|
||||
в шапке файла).
|
||||
|
||||
**Почему Medium, не High:** сам факт защиты подтверждён (мутация красит
|
||||
smoke), это пробел в реестре регресс-тестирования, а не текущий баг.
|
||||
|
||||
### Low (снимаю без правки, с записью)
|
||||
|
||||
1. **AC5 (вложенные комнаты) и AC9 (retarget между двумя разными комнатами
|
||||
во время tween)** не имеют прямого автотеста, обещанного планом
|
||||
тестирования (`demo/smoke_room_fit.mjs` кликает только по одной комнате —
|
||||
`room-a`; вторая комната `outer` в фикстуре есть, но по ней ни разу не
|
||||
кликают; `test/viewport-transition.test.mjs` этим диффом не тронут вообще,
|
||||
хотя план тестирования спеки обещал его расширить). Не поднимаю до Medium:
|
||||
оба свойства следуют по конструкции из переиспользования немодифицированного
|
||||
механизма — `roomFitOwnerFromPath` читает `ev.composedPath()` те же DOM-узлы
|
||||
`[data-hp="room"]`, что уже использует hover (`@pointerenter=${enterRoom}` на
|
||||
тех же элементах, `houseplan-card.ts:11700-11726`), и `_startCameraTransition`
|
||||
(ретаргет от `_cameraState()`, который читает live `_zoom`/`_view`,
|
||||
непрерывно обновляемые во время tween) — код, который #82 уже покрывает для
|
||||
других reason. Проверено чтением, не исполнением.
|
||||
2. **Механический вынос в `src/card-runtime.ts`** (`strictNumber`, `lruRead`/
|
||||
`lruWrite`, `normalizeMarkupTool`, `expiredWarmViewport`, `warmBootKey`,
|
||||
`warmMatch`) побайтно сверен построчным diff с версией на `origin/dev` —
|
||||
логика идентична (только `export`, переименование локальных переменных
|
||||
`s`→`slot`, форматирование), но при переносе потеряны развёрнутые
|
||||
комментарии-обоснования (`DEV-B703-01`, `AUD-159B1-01` и др.), объясняющие
|
||||
*почему* устроено именно так warm-remount кеширование. Не блокирует —
|
||||
история остаётся в `git blame` — но будущим читателям `card-runtime.ts`
|
||||
будет сложнее реконструировать контекст.
|
||||
|
||||
## Таблица «чем краснеет» (защитные AC, PROCESS.md §2.7)
|
||||
|
||||
| AC | Чем доказан | Чем краснеет |
|
||||
|---|---|---|
|
||||
| AC6 (интерактивный владелец подавляет fit) | `node demo/smoke_room_fit.mjs` | `node scripts/mutation-gate.mjs --id=room-fit-interactive-owner-leaks-through` → красный, прогнано, поймано |
|
||||
| AC7 (pan/pinch/long press не создают fit) | `node demo/smoke_room_fit.mjs` | `node scripts/mutation-gate.mjs --id=room-fit-pan-release-reaccepted` → красный, прогнано, поймано |
|
||||
| AC8 (room tap не входит в kiosk double-tap) | `node demo/smoke_room_fit.mjs` | `node scripts/mutation-gate.mjs --id=room-fit-enters-kiosk-double-tap-sequence` → красный, прогнано, поймано |
|
||||
| AC10 (SVG/HTML синхронны на промежуточных кадрах) | `node demo/smoke_room_fit.mjs` | `node scripts/mutation-gate.mjs --id=room-fit-html-overlay-jumps-ahead` → красный, прогнано, поймано |
|
||||
| AC15 (нет zoom-only записи для reason `room`) | `node demo/smoke_room_fit.mjs` (`roomIntentIsSessionOnly`) | **не зарегистрирован**; ручная мутация (снял оба `reason !== 'room'`) → `roomIntentIsSessionOnly: false`, прогнано мной, не воспроизводимо CI до фикса (Medium-2) |
|
||||
| AC11 (повторный fit — no-op) | `node demo/smoke_room_fit.mjs` (`repeatedFitIsNoOp`) | не зарегистрирован; логика (`sameCameraState`-гард в `_startCameraTransition`) не изменена этим диффом, унаследована от #82 — не перепроверял мутацией, см. «чего не проверял» |
|
||||
| AC12 (resize сохраняет focus до ручной команды) | `node demo/smoke_room_fit.mjs` (`resizeRefitsAtomically`, `panCancelsRoomIntent`) | не зарегистрирован; проверено чтением (`_clearRoomFocus()` вызывается во всех перечисленных в ТЗ точках отмены) — не перепроверял мутацией |
|
||||
| AC2/AC3/AC4 (математика полей/zoom/bounds) | `node --test test/room-fit.test.mjs` (pure unit, `test-build/room-fit.js`) | дешёвый unit, я прочитал и мысленно снял защиту (`q: 0.8→1`, удаление clamp) — assertions считают точные числа, любое искажение ломает `close()`/`assert.equal`; не требует мутации по PROCESS.md §2.7 (дешёвый pure unit) |
|
||||
|
||||
## Что проверено и корректно
|
||||
|
||||
- **AC1** (чистый click/tap вписывает и центрирует): `smoke_room_fit.mjs` →
|
||||
`cleanRoomTapFits`, `oneFlatAxisUsesTenPercent` зелёные для mouse; keyboard —
|
||||
`keyboardUsesSameCommand`. Touch/pen отдельно не гонял (смок использует
|
||||
`pointerType: 'mouse'`/`'touch'` в разных секциях — touch есть в kiosk-блоке),
|
||||
pen — проверено чтением: ownership-код (`roomFitOwnerFromPath`,
|
||||
`acceptedRoomFitGesture`) не различает `pointerType`, значит логика
|
||||
идентична для mouse/touch/pen по конструкции.
|
||||
- **AC2** (10% ± 1 px, 2000×1000→800×800): `test/room-fit.test.mjs` точно
|
||||
воспроизводит числовой пример спеки; прогнано, проходит.
|
||||
- **AC3/AC4** (bounds = ровно floor+wall body, Flat/Iso через точные
|
||||
проекции): unit-тесты плюс код-чтение `_roomFitBounds` — использует те же
|
||||
`_innerRoomContour`/`_wallUnionGeometry`/`roomWallProfile`/`outsetContour`,
|
||||
что рендер, без DOM `getBoundingClientRect` и без второй wall-модели
|
||||
(`grep` по диффу — ноль совпадений). Golden — сознательно отложен на
|
||||
пре-бету по плану тестирования спеки.
|
||||
- **AC5**: проверено чтением — общий DOM-путь с hover (см. Low-1).
|
||||
- **AC6/AC7/AC8/AC10**: доказаны и перепроверены мутациями (таблица выше) —
|
||||
все четыре мутанта поймали регресс.
|
||||
- **AC9**: проверено чтением, `_startCameraTransition` и `_cameraState()` не
|
||||
изменены этим диффом кроме добавления reason `room`; `smoke_smooth_zoom.mjs`
|
||||
зелёный подтверждает, что общий контракт ретаргета/cancel не сломан.
|
||||
- **AC11/AC12/AC13**: смок зелёный (`repeatedFitIsNoOp`, `resizeRefitsAtomically`,
|
||||
`panCancelsRoomIntent`); AC13 (invalid geometry/zero stage) — код-чтение
|
||||
`_fitRoom`/`_roomFitBounds` (оба возвращают `null`/no-op на невалидных данных
|
||||
без NaN-путей, `_roomFitBounds`→`roomFitGeometryBounds`→`finitePoint`
|
||||
фильтрует NaN на входе).
|
||||
- **AC14**: role/tabindex/aria-label/focus-visible/Enter-Space работают
|
||||
корректно **в View и kiosk** — `smoke_room_fit.mjs`
|
||||
(`labelIsKeyboardAction`, `keyboardUsesSameCommand`); но см. Medium-1 для
|
||||
Devices/Decor.
|
||||
- **AC15**: гейт работает сейчас (см. Medium-2 про отсутствие мутанта).
|
||||
- **AC16**: bundle:budget зелёный; код-чтение подтверждает, что
|
||||
`_roomFitBounds` вызывается один раз на принятый жест/resize (не в
|
||||
`_stagePointerMove`/RAF), и не читает layout (`getBoundingClientRect`
|
||||
отсутствует).
|
||||
- i18n: `room.fit_action` добавлен с parity en/ru/de/fr.
|
||||
- Документация: `docs/CANVAS.md`, `docs/ISOMETRIC.md`, `docs/TESTING.md`,
|
||||
`docs/USER-GUIDE.ru.md`, оба CHANGELOG — соответствуют реализации, термины
|
||||
совпадают с фактическим поведением (не проверял на разошедшиеся с кодом
|
||||
утверждения, кроме уже отмеченного разрыва в Medium-1, который документация
|
||||
не выдаёт за факт — таблица USER-GUIDE описывает только View).
|
||||
- Фикс-коммит `6a7ef48a` («preserve editor pointer cancellation») — реальная,
|
||||
обоснованная правка регрессии, внесённой первым коммитом этой же задачи
|
||||
(`4aad9fac`), не расширение скоупа: `_stagePointerCancel` раньше считал
|
||||
`viewportGestureEnded` уже ПОСЛЕ того как host обнулял `_pinchStart`/
|
||||
`_panStart`, из-за чего editor-ветка теряла `requestUpdate()` после отмены
|
||||
pan/pinch в редакторе. Перепроверено `smoke_editor_gestures.mjs` (зелёный) —
|
||||
единственный минус: два оператора на одной строке через `;` (стилевая
|
||||
мелочь, не блокирует).
|
||||
- Bundle-budget: поднятие потолка задокументировано в самом файле с числами,
|
||||
согласуется с измеренным результатом (294 862 Б, заявлено 294 864 в
|
||||
комментарии — расхождение 2 Б, в пределах шума сборки).
|
||||
|
||||
## Чего не проверял
|
||||
|
||||
- Полный `demo/smoke_*.mjs` (220 файлов), `npm run golden:capture`/`golden:verify`,
|
||||
`performance_smoke` — по плану тестирования спеки и PROCESS.md §8 это
|
||||
пре-релизный гейт, не гейт этого цикла; я прогнал целевую выборку
|
||||
(smoke-select + AC-именованные + семью `_stagePointerUp`-вызовов) и нашёл в
|
||||
ней реальный красный результат (High-1) — до его исправления смысла гонять
|
||||
остальные 200+ файлов не вижу.
|
||||
- `python -m pytest tests_backend` — diff не трогает `custom_components/**/*.py`.
|
||||
- `npm run invariants` — diff не меняет геометрическую модель (комнаты/стены/
|
||||
layout/marker.space/open_spans); камера не участвует в инвариантах.
|
||||
- Мутации для AC11/AC12 (нет в реестре; логика либо унаследована от #82
|
||||
без изменений в этом диффе — AC11, либо тривиально читается — AC12).
|
||||
- Реальное touch/pen устройство и screen reader — по коду и смоку логика не
|
||||
различает `pointerType`, но живого браузера на телефоне/с NVDA не было.
|
||||
- Изометрический golden-скриншот (см. выше — сознательно отложен).
|
||||
|
||||
## Вердикт
|
||||
|
||||
Один блокирующий High (сломанный существующий пре-релизный смок,
|
||||
воспроизведён на текущем SHA) и два Medium в скоупе (нарушение AC14 в
|
||||
Devices/Decor, отсутствующий обязательный mutation-gate на AC15). Все три —
|
||||
чинятся в этой же задаче без расширения скоупа.
|
||||
|
||||
**Вердикт: красный · заход r1 · блокирующих циклов 1/4 · High: 1 · Medium: 2 → в задаче**
|
||||
|
||||
Возврат в `S6-in-progress`.
|
||||
|
||||
---
|
||||
|
||||
<!-- material-anchors: сгенерировано конвейером (#414) -->
|
||||
|
||||
## Материал раунда
|
||||
|
||||
- Ветка: `issue/152-room-click-fit`, коммит `a54addb694a3` — ребейз его осиротит, и это нормально: ниже якоря, которые ребейз не меняет.
|
||||
- Дерево материала: `042ef7b3005c8e10620427979a9960d36eed0155`
|
||||
```
|
||||
git log --all --format='%H %T' | grep 042ef7b3005c
|
||||
```
|
||||
- ТЗ `docs/specs/152-room-click-fit.md`, блоб `fb036d2bb9cc522555c55e15000d7d693e204f8b`
|
||||
```
|
||||
git log --all --find-object=fb036d2bb9cc522555c55e15000d7d693e204f8b -- docs/specs/152-room-click-fit.md
|
||||
```
|
||||
Reference in New Issue
Block a user