mirror of
https://github.com/Matysh/houseplan-card
synced 2026-10-03 21:28:59 +00:00
Проверка (CI) / Предполётные проверки: документация, провенанс, процесс (push) Failing after 1m5s
Проверка (CI) / Классификация изменённых файлов (push) Successful in 22s
Проверка (CI) / HACS: валидация репозитория (push) Failing after 25s
Проверка (CI) / Переиспользование: это дерево уже проверено (push) Successful in 42s
Проверка (CI) / Hassfest: манифест интеграции (push) Failing after 16s
Проверка (CI) / Бэкенд: pytest в Home Assistant (push) Failing after 7m7s
Проверка (CI) / Фронтенд: типы, юниты, мутанты, синхрон бандла (push) Failing after 8m25s
Проверка (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
Issue: #447 User-Visible: no
254 lines
21 KiB
Markdown
254 lines
21 KiB
Markdown
# CODE-REVIEW-447-r1
|
||
|
||
Issue: #447 · Этап: code · Заход: r1 · блокирующих циклов израсходовано 0 из 4
|
||
|
||
Материал: `git diff origin/dev...HEAD` на коммите `f62dd6b6` (HEAD ветки
|
||
`issue/447-exterior-snap-keyboard`, `git merge-base origin/dev HEAD` совпадает
|
||
с `origin/dev` — ветка не отстаёт и не расходится).
|
||
|
||
Коммиты в диапазоне:
|
||
- `a1fbea07`, `6c668d4b`, `818ad2d2`, `62ecb856` — спецификация и её ревью
|
||
(spec-этап, уже пройден зелёным в r2, здесь не пересматривается);
|
||
- `6b07b70d` — продуктовый код, тесты, документация, changelog
|
||
(`Issue: #447`, `User-Visible: yes`);
|
||
- `f62dd6b6` — принятый screenshot fingerprint (`Issue: #447`,
|
||
`User-Visible: no`).
|
||
|
||
## Скоуп проверки
|
||
|
||
ТЗ: `docs/specs/447-exterior-furniture-snap-keyboard-nudge.md`, прошло
|
||
независимое spec-ревью r2 зелёным (H1 r1 закрыт). Код-ревью проверяет
|
||
реализацию против контракта §1–§4 и AC1–AC9 этого файла.
|
||
|
||
Изменённые продуктовые файлы:
|
||
- `src/furniture-wall-surface.ts` — парная наружная поверхность внешнего
|
||
атома;
|
||
- `src/furniture-placement.ts` — `sideScore` → eligibility-фильтр для парных
|
||
поверхностей;
|
||
- `src/editors/decor/geometry.ts` — `nudgeDecorShape`, чистый расчёт
|
||
клавиатурной дельты;
|
||
- `src/houseplan-card.ts` — Arrow-guard в `_keyHandler`, `_decorNudge`;
|
||
- `test/furniture.test.mjs`, `test/decor-geometry.test.mjs` — новые unit;
|
||
- `demo/smoke_furniture.mjs`, `demo/smoke_decor.mjs` — новые browser-сценарии;
|
||
- `scripts/mutation-gate.mjs` — три новых дорогих мутанта;
|
||
- `docs/FURNITURE.md`, `docs/DECOR-EDITOR.md`, `docs/USER-GUIDE.ru.md`,
|
||
оба CHANGELOG, `docs/images/screenshots.json`.
|
||
|
||
## Как проверялось
|
||
|
||
Зелёного Validate на `f62dd6b6` не найдено, поэтому дешёвые гейты прогнаны
|
||
самостоятельно, на этом SHA:
|
||
|
||
| Гейт | Результат |
|
||
|---|---|
|
||
| `npx tsc --noEmit` | зелёный, без вывода |
|
||
| `npm test` | 1889 тестов, 1888 passed, 0 failed, 1 skipped (совпадает с числом из отчёта реализации) |
|
||
| `npm run build` | собрался, 11.8s |
|
||
| `npm run bundle:sync` | зелёный; `git status` после — чисто, обе поставляемые копии (`custom_components/.../frontend`, `dist/`) побайтно совпадают с закоммиченными |
|
||
| `npm run bundle:budget` | зелёный: initial View 293220 B gzip / потолок 294000 B; предупреждение о запасе 6780 Б — тот же долг #367, не новый |
|
||
| `node scripts/check-docs.mjs` | зелёный (диф трогает `src/**` → обязателен); `docs/images/screenshots.json` до/после: `imageSha256` всех 10 сценариев не изменился, изменился только `sourceFingerprint/sourceSha256` — переотпечаток без визуальной регрессии |
|
||
| `node scripts/no-new-any.mjs --base origin/dev --head HEAD` | 117 новых строк, новых `any` нет |
|
||
| `node scripts/mutation-gate.mjs --check` | все мутанты реестра, включая три новых (`furniture-exterior-surface-removed`, `decor-keyboard-nudge-reruns-magnet`, `decor-keyboard-nudge-drops-focus-dialog-guards`) — `ok` (патчи применимы, guard-файлы существуют); дорогой прогон (пересборка бандла на мутанте) не повторялся — это предрелизный гейт, автор уже прогнал и получил красный→зелёный на каждом |
|
||
| `node scripts/smoke-select.mjs --base origin/dev --head HEAD` | см. раздел ниже |
|
||
| `node demo/smoke_furniture.mjs` | зелёный, все поля `true`, включая три новых (`exteriorPreviewStaysOutside`, `exteriorCommitMatchesPreview`, `exteriorExactAxisDragPreservesSide`) |
|
||
| `node demo/smoke_decor.mjs` | зелёный, все поля `true`, включая девять новых Arrow-полей |
|
||
| `npm run invariants` | не прогонялся: диф не трогает persisted geometry/`layout`/wall thickness records/`marker.space`/`open_spans` — новые поверхности transient editor-only (подтверждено чтением `furnitureWallSurfacesFor`: кэш по `_cfgEpoch`, не пишется в конфиг) |
|
||
| `python -m pytest tests_backend` | не прогонялся: `custom_components/**/*.py` не тронут (только сгенерированный frontend-бандл) |
|
||
|
||
### smoke-select
|
||
|
||
```
|
||
Изменено файлов src/**: 4 · символов проекта на изменённых строках: 27
|
||
Матрица: 219 смоков · порог «широкого» символа: больше 43 смоков
|
||
Прямое совпадение (30): ...
|
||
Слабая связь (40): ...
|
||
Не учитывались как широкие: _gridPitch
|
||
```
|
||
|
||
Решение по каждой строке «прямого совпадения»: не прогонял ни один из
|
||
оставшихся 28, кроме уже расширенных `smoke_furniture.mjs` и
|
||
`smoke_decor.mjs`. Основание: общие символы совпадения — `_curSpaceCfg`,
|
||
`_saveConfig`, `_mode`, `_decorTool`, `_decorList`, `_decorMove`, `_decorSel`,
|
||
`_decorDraft`, `_dtDrag`, `_bdDrag`, `_geometrySnapshot`, `NORM_W` — это
|
||
состояние общего editor-harness, которое называет почти любой decor/furniture
|
||
смок независимо от темы; ни один из них не читает `roomFurnitureWallSurfaces`,
|
||
`snapFurnitureToWall` или клавиатурную дельту. Показательно, что сам
|
||
`_gridPitch` — тот единственный символ, который централен именно для AC4 —
|
||
инструмент исключил как «слишком широкий» (пересекает порог в 43 смока) и в
|
||
список совпадений вообще не попал; это подтверждает, что список совпадений
|
||
управляется общими полями оркестрации, а не темой задачи. `smoke_grid_snap.mjs`
|
||
и `smoke_drag_bounds.mjs` проверяют pointer-snap и pointer-drag границы —
|
||
не Arrow-путь; AC6 (bounds/no-deform для клавиатурного сдвига) уже доказан
|
||
unit-тестом с проверяемой падаемостью (см. ниже), отдельный browser-повтор не
|
||
добавляет покрытия для этой дельты. Слабые связи (40 позиций) не
|
||
просматривались — все совпадения там по одному распространённому имени
|
||
(`_mode`, `_curSpaceCfg` и т.п.), гейт называет это поводом посмотреть, не
|
||
обязанностью прогонять.
|
||
|
||
### Дисциплина «тест должен уметь падать»
|
||
|
||
Проверено для тестов, которые я прогонял, локальной мутацией с последующим
|
||
`git checkout --`:
|
||
|
||
- `const exterior = ownersByAtom.get(atomId)?.size === 1 && half > 1e-9;` →
|
||
`const exterior = false;` — `npm test` красит 4 теста (AC1/AC2/AC3-смежные
|
||
unit в `test/furniture.test.mjs`);
|
||
- `const wantedX = renderDx / canvasW, wantedY = renderDy / canvasH;` →
|
||
`const wantedX = renderDx, wantedY = renderDy;` в `nudgeDecorShape` —
|
||
`test/decor-geometry.test.mjs` красит тест «keyboard nudge moves every decor
|
||
kind by one visible cell...» (AC4), причём падает именно на масштабе
|
||
(`AssertionError: expected true, actual false` в `close()`), а не на
|
||
постороннем поле.
|
||
|
||
Оба файла возвращены в исходное состояние, `git status` после — чисто.
|
||
|
||
## Находки
|
||
|
||
Не найдено ни одной находки — ни High, ни Medium, ни Low.
|
||
|
||
## Проверено и корректно
|
||
|
||
- **AC1 (наружная поверхность).** `roomFurnitureWallSurfaces` группирует
|
||
атомы по `ownersByAtom` (ключ — канонический `segmentIdentity(axisA, axisB)`,
|
||
инвариантный к порядку точек), и только атом с ровно одним владельцем и
|
||
`half > 1e-9` получает вторую, наружную поверхность
|
||
(`src/furniture-wall-surface.ts:92-108`). Наружная поверхность построена как
|
||
`axis - inwardNormal*half` с нормалью `-inwardNormal` — ровно то, что требует
|
||
§1 ТЗ. Внутренняя поверхность (уже существовавшая) сохранена без изменений
|
||
геометрии, только помечена `roomSide: 'inside'`.
|
||
- **`snapFurnitureToWall` — фильтр, не тай-брейк, только для парных
|
||
поверхностей.** `if (surface.roomSide) continue;` на стороне «за стеной»
|
||
(`src/furniture-placement.ts:108-114`) полностью убирает поверхность из
|
||
участия при равном/меньшем расстоянии — то, что r1 spec-ревью потребовало
|
||
явно решить (§2 ТЗ: «фильтр, не тай-брейк»). Общие/нулевые/independent
|
||
поверхности (`surface.roomSide === undefined`) не проходят через `continue`
|
||
и сохраняют старое поведение `sideScore = -2` — регрессии для #445 нет,
|
||
подтверждено тестом «a shared thick wall selects the intent side...» (не
|
||
изменён логически, только дополнен проверкой `roomSide === undefined`).
|
||
- **AC2 (общая/нулевая/independent стена).** Новый тест «partially shared
|
||
edges are atomised before exterior ownership is assigned» бьёт по главному
|
||
риску ТЗ («принять частично общий атом за внешний»): вертикальная стена
|
||
100→400 с частичным перекрытием комнатой `side` на 200→300 даёт `shared`
|
||
(2 поверхности, `roomSide === undefined`) ровно на пересекающемся
|
||
интервале и `inside+outside` на обоих внешних огрызках 100–200 и 300–400.
|
||
Существующий тест инвариантности к порядку/winding/surface order не
|
||
затронут и остаётся зелёным.
|
||
- **Exact-axis tie-break.** Новый placement на оси без `preferredNormal`
|
||
детерминированно выбирает `inside` за счёт вторичного тай-брейка
|
||
`stableId.localeCompare` (`":inside" < ":outside"` лексикографически) —
|
||
устройство неэлегантное, но корректное и покрыто тестом «new exact-axis
|
||
exterior placement defaults inside while drag preserves either side»; тот же
|
||
тест подтверждает, что drag с `preferredNormal=[0,-1]` (наружу) сохраняет
|
||
наружную сторону.
|
||
- **AC3 (preview/commit/drag).** `demo/smoke_furniture.mjs` прогнан лично:
|
||
наружный pointer даёt preview снаружи, `pointerdown` коммитит идентичную
|
||
геометрию, drag с наружной стороны на ось сохраняет сторону — все три поля
|
||
`true`.
|
||
- **AC4 (клавиатурная дельта, единый источник масштаба).** `_decorNudge`
|
||
вызывает `nudgeDecorShape(selected, renderDx, renderDy, NORM_W,
|
||
this._decorH, CANVAS_LIMIT)` (`src/houseplan-card.ts:8460-8463`); `_decorH`
|
||
— геттер, буквально возвращающий `NORM_W` (`src/houseplan-card.ts:8342-8344`,
|
||
холст квадратный), поэтому `renderDx/NORM_W` и `renderDy/this._decorH`
|
||
вычисляют то же `GRID_STEP_N`, что уже использует mouse-drag
|
||
`_decorMoveUpdate` (`src/houseplan-editor-runtime.ts:4353-4354`) — то самое
|
||
требование r1 spec-ревью (H1) выполнено в коде, не только в тексте ТЗ.
|
||
Unit-тест использует те же экспортированные константы (`GRID_PITCH`,
|
||
`GRID_STEP_N`, `NORM_W`, `CANVAS_LIMIT`) из `space-geometry.ts`, а не
|
||
захардкоженные числа — «одно число, один источник» соблюдено.
|
||
- **`nudgeDecorShape` не деформирует и не пересчитывает снап.** Для линии оба
|
||
конца получают одну дельту; для box-фигур меняются только `x/y`, не
|
||
`w/h/angle`; функция явно документирована как «does not know about grid or
|
||
wall snapping». Off-grid фаза сохраняется (дельта прибавляется, не
|
||
округляется) — проверено тестом с дробным начальным `x=0.1013`.
|
||
- **AC5 (без повторного магнита).** `demo/smoke_decor.mjs` подставляет шпионы
|
||
на `_decorSnap` и `_editorRuntime._furnMoveUpdate` вокруг Arrow-нажатий и
|
||
проверяет `decorSnapCalls === 0 && furnitureMoveCalls === 0` — резолверы
|
||
магнита физически не вызываются, это не косвенный вывод по результату.
|
||
Соответствующий мутант `decor-keyboard-nudge-reruns-magnet` (вызов
|
||
`_decorSnap` внутри `_decorNudge`) ловится именно этим шпионом.
|
||
- **AC6 (bounds без деформации).** Тест «keyboard nudge preserves line
|
||
geometry and clamps the whole object at canvas bounds» проверяет и линию
|
||
(длина/направление неизменны у границы), и повёрнутый на 45° rect через
|
||
`boxCorners` (реальные повёрнутые углы, не осевой bbox) — последний шаг
|
||
короче клетки, следующий — `null` (no-op). Падаемость теста подтверждена
|
||
вручную (см. выше).
|
||
- **AC7 (history/save = одно нажатие).** `demo/smoke_decor.mjs`:
|
||
`arrowIsOneHistoryAndSaveTransaction` проверяет `_geometryHistory.size===1`,
|
||
имя записи `history.decor_move`, ровно один вызов debounced-save и рост
|
||
`_cfgEpoch` на 1 за один keydown; `boundaryNoopIsConsumedButNotSaved`
|
||
проверяет, что no-op у границы не трогает ни историю, ни `_cfgEpoch`, ни
|
||
счётчик save. Undo/Redo восстанавливают ровно один шаг
|
||
(`arrowUndoRedoIsExactlyOneStep`), selection не теряется.
|
||
- **AC8 (поля/диалоги/жесты/модификаторы).** Guard в `_keyHandler`
|
||
(`src/houseplan-card.ts:3024-3034`) переиспользует существующий
|
||
`inField`/`inEditorSecondary`/`_editorSecondaryDialogBlocked` паттерн, уже
|
||
проверенный для Delete/Backspace двумя строками выше. Смок отдельно проверил
|
||
`input/textarea/select/contenteditable`, toolbar-кнопку, открытый
|
||
`_decorEraseConfirm`, активный `_decorMove` и `Ctrl/Cmd/Alt+Arrow` — все не
|
||
меняют декор. `Shift+Arrow` даёт тот же один шаг (`mod` не включает
|
||
`shiftKey`, что видно по определению `const mod = e.ctrlKey ||
|
||
e.metaKey`) — соответствует явному решению владельца «ускоренного шага по
|
||
Shift нет».
|
||
- **AC9 (совместимость и бюджет).** Все перечисленные в AC9 гейты
|
||
(`typecheck`, `test`, `build`, `bundle:sync`, `bundle:budget`, `check-docs`)
|
||
зелёные, включая мой независимый прогон, не только отчёт автора.
|
||
`furnitureWallSurfacesFor` кэширует по `[space.id, _cfgEpoch, _cellCm,
|
||
_gridPitch, _wallKeyPitch]` — как и раньше, один пересчёт на geometry epoch,
|
||
новый парный кандидат не меняет модель кэширования.
|
||
- **Trailers и changelog.** `6b07b70d`: `Issue: #447`, `User-Visible: yes`, в
|
||
том же коммите правки `docs/CHANGELOG.md` и `docs/CHANGELOG.ru.md`.
|
||
`f62dd6b6`: `Issue: #447`, `User-Visible: no` — верно для чисто
|
||
fingerprint-коммита без видимого изменения (все 10 `imageSha256`
|
||
побайтно прежние).
|
||
- **Терминология.** «Редактор подложки», «Выбрать», формулировки про стрелки
|
||
в `docs/USER-GUIDE.ru.md` и `docs/DECOR-EDITOR.md` согласованы друг с
|
||
другом и с уже принятой в проекте терминологией интерфейса.
|
||
- **Одно число — один источник.** Диф не добавляет новую видимую пользователю
|
||
величину, показанную в двух местах (badge длины/площади не пересчитывается
|
||
по-новому — nudge просто сдвигает существующую геометрию, которую badge и
|
||
так читает из того же объекта). `test/single-source-numbers.test.mjs` в
|
||
общем прогоне `npm test` зелёный.
|
||
|
||
## Не проверял
|
||
|
||
- Дорогой прогон `scripts/mutation-gate.mjs` (пересборка бандла на каждом из
|
||
трёх новых мутантов) — это предрелизный гейт (PROCESS.md §8); ограничился
|
||
`--check` (применимость патчей) и подтвердил силу соответствующих unit/smoke
|
||
вручную отдельной локальной мутацией не из реестра (см. «тест должен уметь
|
||
падать» выше). Автор в комментарии сообщил, что все три мутанта дали
|
||
красный→зелёный переход при полном прогоне.
|
||
- Полный набор `demo/smoke_*.mjs` (219 файлов), golden (`npm run
|
||
golden:verify`) и performance-профили — не названы в AC и не запускались;
|
||
задача не меняет статическую композицию готового плана (только
|
||
интерактивное позиционирование, уже покрытое DOM-coordinate смоками), это
|
||
явно оговорено в разделе «План тестирования» ТЗ.
|
||
- 28 из 30 «прямых совпадений» `smoke-select` — решение и обоснование см. в
|
||
разделе «smoke-select» выше; 40 «слабых связей» не просматривались.
|
||
- `npm run invariants` и backend pytest — диф не касается persisted-геометрии
|
||
и `custom_components/**/*.py`, гейты неприменимы (обоснование выше).
|
||
- Ручное тестирование в браузере не проводилось (не входит в цикл ревью);
|
||
вопрос «оно вообще работает» закрыт чтением кода плюс самостоятельным
|
||
прогоном `demo/smoke_furniture.mjs`/`demo/smoke_decor.mjs`, которые управляют
|
||
реальным production-бандлом через Playwright, а не мок.
|
||
|
||
## Вывод
|
||
|
||
AC1–AC9 доказаны кодом и тестами, которые я перепроверил лично (прогон плюс
|
||
целевая мутация для двух самых нагруженных AC). High/Medium находок нет.
|
||
|
||
---
|
||
|
||
<!-- material-anchors: сгенерировано конвейером (#414) -->
|
||
|
||
## Материал раунда
|
||
|
||
- Ветка: `issue/447-exterior-snap-keyboard`, коммит `f62dd6b632cf` — ребейз его осиротит, и это нормально: ниже якоря, которые ребейз не меняет.
|
||
- Дерево материала: `f6a04165e64ca68009db103a9a8ee83426aff47d`
|
||
```
|
||
git log --all --format='%H %T' | grep f6a04165e64c
|
||
```
|
||
- ТЗ `docs/specs/447-exterior-furniture-snap-keyboard-nudge.md`, блоб `5c0036470b3bd675147ad5dc308b8d760be18b9c`
|
||
```
|
||
git log --all --find-object=5c0036470b3bd675147ad5dc308b8d760be18b9c -- docs/specs/447-exterior-furniture-snap-keyboard-nudge.md
|
||
```
|