Files
houseplan-card/docs/reviews/CODE-REVIEW-449-r2.md
T
claude[bot] 9e56842bc1
Проверка (CI) / Классификация изменённых файлов (push) Successful in 37s
Проверка (CI) / HACS: валидация репозитория (push) Skipped
Проверка (CI) / Hassfest: манифест интеграции (push) Skipped
Проверка (CI) / Фронтенд: типы, юниты, мутанты, синхрон бандла (push) Skipped
Проверка (CI) / Переиспользование: это дерево уже проверено (push) Successful in 36s
Проверка (CI) / Предполётные проверки: документация, провенанс, процесс (push) Failing after 57s
Проверка (CI) / Смоки в браузере (шард 1 из 3) (push) Skipped
Проверка (CI) / Смоки в браузере (шард 2 из 3) (push) Skipped
Проверка (CI) / Смоки в браузере (шард 3 из 3) (push) Skipped
Проверка (CI) / Смоки: все шарды зелёные (push) Skipped
Проверка (CI) / Golden-кадры против принятых эталонов (push) Skipped
Проверка (CI) / Перф-смок: бюджет времени кадра (push) Skipped
Проверка (CI) / Бэкенд: pytest в Home Assistant (push) Skipped
docs: review document for #449
Issue: #449
User-Visible: no
2026-09-04 16:23:07 +00:00

190 lines
18 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.
# CODE-REVIEW-449-r2
- Issue: https://github.com/Matysh/houseplan-card/issues/449
- Этап: код-ревью (PROCESS.md §2.7), заход r2, блокирующих циклов израсходовано 1/4
- ТЗ: `docs/specs/449-double-fit-all.md` (трек полный)
- Материал: `05ef3181` (SHA r1) → `aa51ed2b` (`fix: complete double background fit delivery`) → `1d8edf4a` (`docs: refresh screenshot fingerprint for double fit`)
- SHA материала этого раунда: `1d8edf4a016736eb1ee6e865ab6d686cec0ac403`
- Ветка: `issue/449-double-fit-all`
- Предыдущий раунд: `docs/reviews/CODE-REVIEW-449-r1.md`, вердикт **красный**, High: 3, Medium: 2, Low: 1, получен на SHA `05ef318133ca54b1f702844253d06edd5a665c5e`
## Скоуп
Разбор — по дельте (PROCESS.md §2.9/§2.10): между SHA r1 и текущим HEAD два
коммита, ребейза не было (`git merge-base 05ef3181 HEAD` = `05ef3181`),
контракт поведения не менялся, новая подсистема не задета — дельта локальна к
трём High и двум Medium предыдущего раунда.
`git diff --stat 05ef3181..HEAD` (30 файлов, из них 374 строки — это сам
документ r1, коммитящийся автоматически, не код автора):
- `src/houseplan-card.ts` — 2 содержательные правки: `_doubleFitEnabled`
больше не проверяет `!_suppressClick`; `pointerUp` получает явный
`blocked`-аргумент `this._suppressClick || …`, вычисляемый непосредственно в
момент отпускания.
- `scripts/bundle-budget.mjs` — `INITIAL_VIEW_GZIP_CEILING` 296 000 → 297 000,
с комментарием-обоснованием запаса.
- `scripts/mutation-gate.mjs` — удалён мутант
`room-fit-enters-kiosk-double-tap-sequence` (ослепший свидетель из M2 r1).
- `dist/**`, `custom_components/houseplan/frontend/**` — пересборка и
синхронизация (закрывает High-1).
- `docs/images/screenshots.json` — обновлённый фингерпринт (закрывает
High-2).
- `docs/reviews/CODE-REVIEW-449-r1.md` — сам документ прошлого раунда,
коммитится конвейером, не тронут вручную.
Тестовые файлы (`test/room-fit.test.mjs`, `demo/smoke_*.mjs`) не менялись —
новых unit-доказательств для правки гонки не добавлено, вместо этого
перепроверен уже существующий смок, названный ТЗ как доказательство AC3 (см.
ниже).
## Закрытие раунда r1
| Находка r1 | Чем закрыта | Где это видно |
|---|---|---|
| **High-1** — `custom_components/houseplan/frontend/**` не синхронизирован с `dist/**`, фича не доставлена | `npm run bundle:sync` выполнен и закоммитен в `aa51ed2b` | `cmp dist/houseplan-card.js custom_components/houseplan/frontend/houseplan-card.js` → идентичны (перепроверено: свежая пересборка `npm run build` на HEAD не меняет рабочее дерево — `git status --short` пуст) |
| **High-2** — `docs`-гейт красный, фингерпринт скриншотов устарел | Коммит `1d8edf4a`, `docs/images/screenshots.json` пересчитан | `node scripts/check-docs.mjs` → `Documentation checks passed (7 files, 12 external links)`, exit 0 (перепрогнано) |
| **High-3** — `smoke_kiosk_pan_lock.mjs`: `doubleTapStillResetsZoom` красный из-за гонки `_suppressClick` ↔ double-tap | `_doubleFitEnabled` больше не гейтит взвод (`pointerDown`) через `_suppressClick`; блокировка при завершении (`pointerUp`) читает `this._suppressClick` напрямую и синхронно, без зависимости от порядка со сброшенным в этом же `pointerDown` флагом | `node demo/smoke_kiosk_pan_lock.mjs` → **`doubleTapStillResetsZoom: true`**, весь сценарий 4/4 OK (перепрогнано, было `expected true, got false`) |
| **Medium-1** — запас bundle-budget ниже шумовой полосы (257 Б < 500 Б) | `INITIAL_VIEW_GZIP_CEILING` 296 000 → 297 000 в `aa51ed2b`, с задокументированным расчётом | `npm run bundle:budget` → `295746 B gzip (потолок 297000 B ±2000, headroom 4254 B)`; `npm test` — тест `#438` зелёный (перепрогнано) |
| **Medium-2** — мутант `room-fit-enters-kiosk-double-tap-sequence` ничего не ловит (устаревшее место защиты) | Мутант удалён из `scripts/mutation-gate.mjs` (один из двух вариантов исправления, предложенных r1: «удалить или перенаправить») | `grep -n "room-fit-enters-kiosk-double-tap-sequence" scripts/mutation-gate.mjs` → пусто; функциональное покрытие того же контракта остаётся в unit-тесте `#449 moved, cancelled, multitouch, foreign-owner…` и в полях смока `kioskRoomTapDoesNotEnterDoubleTap` (оба зелёные, см. ниже) |
| **Low-1** — AC5 для device/opening доказана только чтением, не смоком | Не тронуто в этой дельте — оставлено с той же пометкой, автор не заявлял правку | Инвентаризация не изменилась, см. «Унаследовано из r1» |
## Унаследовано из r1 (без повторной проверки)
Дельта не затрагивает следующие поверхности — их вывод принят из
`docs/reviews/CODE-REVIEW-449-r1.md` (SHA `05ef318133ca`) без повтора:
- Архитектура `DoubleFitGestureRecognizer` / `beginDoubleFitPointer` /
`completeDoubleFitPointer` как чистых функций без DOM/side-effects — код
этих функций не менялся в дельте.
- `planGestureOwnerFromPath` и единый owner-контракт с #152 — `src/room-fit.ts`
не входит в дельту.
- AC1, AC2, AC4, AC6, AC7, AC8, AC9, AC10 — доказательства (unit-таблицы,
мутанты `double-fit-*`, `smoke_room_fit.mjs`, `smoke_smooth_zoom.mjs`) их
код не менялся; тем не менее 4 мутанта #449 и оба смока перепрогнаны в этом
раунде заодно с AC3/AC11 (см. таблицу гейтов) и остаются зелёными — это не
«наследование вслепую», а побочная переподтверждение той же дельтой правки в
соседних строках того же файла.
- Low-1 (AC5 device/opening — доказательство чтением) — не переоценивалось,
дельта его не касается.
- `docs/CANVAS.md`, `docs/TOUCH-SUPPORT.md`, `docs/UX-MODES.md`,
`docs/USER-GUIDE.md(.ru)`, оба `CHANGELOG` — не менялись в этой дельте,
содержание принято как проверенное в r1.
## Как проверялось — гейты этого раунда
Зелёного Validate на SHA `1d8edf4a` нет — все гейты ниже прогнаны локально.
| Гейт | Команда | Результат |
|---|---|---|
| Типы | `npx tsc --noEmit` | зелёный |
| Юниты | `npm test` | **зелёный**: 1913 pass / 0 fail / 1 skip (пропуск — известный `#281` private fixture, не относится к #449); все 3 красных теста r1 (`#438`, `#349` манифест бандла, release ZIP) теперь проходят |
| Сборка | `npm run build` | зелёный, `git status --short` после сборки пуст — бандл детерминированно воспроизводит закоммиченный |
| Три копии бандла | `cmp dist/houseplan-card.js custom_components/houseplan/frontend/houseplan-card.js` | зелёный: идентичны |
| Синхронизация стенда | `npm run bundle:sync` (нужна демо-смокам, `demo/srv/assets` не коммитится) | зелёный, `git status --short` пуст — ничего не разошлось с закоммиченным |
| docs-гейт | `node scripts/check-docs.mjs` | зелёный: «Documentation checks passed (7 files, 12 external links)» |
| any-гигиена | `node scripts/no-new-any.mjs --base origin/dev --head HEAD` | зелёный: «Новых any нет» |
| bundle-budget | `npm run bundle:budget` | зелёный: 295746 Б / потолок 297000 ±2000, headroom 4254 Б (в тесте `#438` запас на обеих границах полосы выше 500 Б) |
| Смок AC3 (ранее красный) | `node demo/smoke_kiosk_pan_lock.mjs` | **зелёный**: `doubleTapStillResetsZoom: true`, весь набор 23/23 |
| Мутанты #449 (4, затронуты соседними строками) | `node scripts/mutation-gate.mjs --id=<id>` × 4 | все 4 «покраснел, как обязан» |
| Подбор смоков по дельте r1→HEAD | `node scripts/smoke-select.mjs --base 05ef3181 --head HEAD` | «Прямое совпадение» — 23 файла (сузилось с 35 у полной задачи — символы дельты: `_suppressClick`, `_pinchStart`, `_panLock`, `_holdFired`, `_mode`, `_modeTransitionBusy`, `_continuity`, `_vacFit`) |
| Прямые смоки (23 из «прямое совпадение») + `smoke_room_fit.mjs`, `smoke_kiosk.mjs`, `smoke_smooth_zoom.mjs` (канонические для #449, отмечены инструментом только слабой связью по `_mode`) | `node demo/smoke_*.mjs` по каждому (26 всего) | все 26 PASS |
| `golden:verify` | не прогонялся | дельта не меняет разметку/геометрию/стили — только арбитраж жеста и числовые константы; наследует вывод r1 |
| `pytest tests_backend` | не прогонялся | дельта не касается `custom_components/**/*.py` |
| model-invariants | не прогонялся | дельта не меняет геометрию/`layout`/толщину стен |
| «Слабая связь» smoke-select (33 файла, не считая трёх названных выше) | не прогонялись | символы (`_mode`, `_modeTransitionBusy`) относятся к неизменённым ветками логики этих файлов — то же решение, что в r1 |
Working tree после каждого гейта, менявшего файлы (`npm run build`,
`npm run bundle:sync`, `mutation-gate`), возвращён к состоянию коммита; в
репозитории изменений не оставлено (`git status --short` — пусто).
## Находки
Новых находок нет. Все 3 High и оба Medium раунда r1 закрыты по существу (см.
«Закрытие раунда r1»), закрытие перепроверено запуском гейтов, а не со слов
автора — правка не описана отдельным комментарием в issue, поэтому весь
разбор в этом документе получен из чтения `git diff 05ef3181..HEAD` и
перезапуска гейтов, а не пересказом чужого резюме.
Отдельно проверено, что исправление гонки High-3 не открыло новую дыру:
`_suppressClick` по-прежнему учитывается в `blocked`-аргументе `pointerUp` —
это защищает случай короткого движения (4–8 px, до срабатывания
`_panLock`/`_pinchStart`), где раньше единственной защитой был именно этот
флаг. Убрана только его роль в geiter'е `_doubleFitEnabled`, используемом на
`pointerDown` (взвод), — то есть ровно то место, где гонка и была: значение
`_suppressClick` на входе в `pointerDown` могло быть устаревшим, потому что
сам `pointerDown` сбрасывает флаг в 6792 строке уже ПОСЛЕ вызова
`_doubleFit.pointerDown(...)` на 6746 строке. Проверено чтением и подтверждено
перепрогоном ранее красного смока.
## Таблица AC → чем доказан → статус (только AC, затронутые дельтой)
| AC | Доказательство | Статус |
|---|---|---|
| AC3 kiosk parity | `smoke_kiosk.mjs` PASS, `smoke_kiosk_pan_lock.mjs` — все 23 поля PASS, включая `doubleTapStillResetsZoom` | **PASS** (было FAIL в r1) |
| AC11 perf/bundle budget | `no-new-any` зелёный; `npm run bundle:budget` зелёный с явно достаточным запасом (743/1254 Б от границ полосы); `npm test` bundle-assets (`#438`) зелёный | **PASS** (было FAIL в r1) |
Остальные AC1, AC2, AC4–AC10 — см. «Унаследовано из r1»; их доказательства
перепрогнаны попутно (мутанты, `smoke_room_fit.mjs`, `smoke_smooth_zoom.mjs`)
и остаются зелёными.
## Что проверено и корректно
- Правка гонки `_suppressClick` устраняет именно тот механизм, который был
разобран в H3 r1 (порядок вызовов внутри `_stagePointerDown`), а не
маскирует симптом таймаутом или увеличением окна.
- `INITIAL_VIEW_GZIP_CEILING` поднят с задокументированным расчётом запасов
по обеим границам полосы — соответствует практике ратчета `#367`,
упомянутой в r1.
- Удаление мутанта `room-fit-enters-kiosk-double-tap-sequence` — чистое,
без остаточных ссылок; функциональное покрытие того же контракта
(room-tap не входит в kiosk double-tap sequence) остаётся в unit-тесте и
смоке, оба перепрогнаны зелёными.
- Три копии бандла (`dist`, `custom_components/houseplan/frontend`,
рабочий стенд `demo/srv/assets`) синхронизированы и воспроизводятся
детерминированно с закоммиченным деревом.
- Оба фикс-коммита несут `Issue: #449` и `User-Visible: no` — обоснованно:
они не меняют заявленное пользователю поведение (уже описанное в
CHANGELOG фичевым коммитом), а чинят доставку и внутреннюю гонку.
## Чего не проверял
- **`npm run golden:verify`** — не прогонял. Дельта не меняет разметку,
геометрию или стили; правка касается только арбитража жеста и двух
числовых констант. Риск визуальной регрессии нулевой; предрелизный гейт
прогонит его перед бетой.
- **`python -m pytest tests_backend`** — не прогонял, дельта не касается
`custom_components/**/*.py` по содержанию (только пересобранный бандл).
- **`node scripts/model-invariants.mjs`** — не прогонял, дельта не меняет
геометрию/`layout`/толщину стен.
- **«Слабая связь» smoke-select (33 файла)** — не прогонял, символы
(`_mode`, `_modeTransitionBusy`) относятся к неизменённым дельтой ветвям
этих файлов.
- **Полный набор `demo/smoke_*.mjs` (221 файл)** — не прогонял целиком, это
предрелизная обязанность (§8), не гейт ревью.
- **AC5 (device/opening double-fit) отдельным смоком** — не перепроверял,
дельта его не касается; наследует Low-1 из r1 как есть.
- **Реальное устройство/браузер** — вне цикла ревью (PROCESS.md §2.7).
## Итог
Все 3 High и оба Medium раунда r1 закрыты и перепроверены запуском
соответствующих гейтов (не со слов автора). Новых находок дельта не внесла.
High: 0, Medium: 0 → вердикт зелёный.
---
<!-- material-anchors: сгенерировано конвейером (#414) -->
## Материал раунда
- Ветка: `issue/449-double-fit-all`, коммит `1d8edf4a0167` — ребейз его осиротит, и это нормально: ниже якоря, которые ребейз не меняет.
- Дерево материала: `87b750f7709b129fda0a1f4d9c16749b751e7a1b`
```
git log --all --format='%H %T' | grep 87b750f7709b
```
- ТЗ `docs/specs/449-double-fit-all.md`, блоб `d6ddccbb3920bdffbb8414f4b6c225a4cdf4f420`
```
git log --all --find-object=d6ddccbb3920bdffbb8414f4b6c225a4cdf4f420 -- docs/specs/449-double-fit-all.md
```