mirror of
https://github.com/Matysh/houseplan-card
synced 2026-10-02 21:01:21 +00:00
343 lines
30 KiB
Markdown
343 lines
30 KiB
Markdown
# 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
|
||
```
|