mirror of
https://github.com/Matysh/houseplan-card
synced 2026-10-03 13:18:58 +00:00
240 lines
19 KiB
Markdown
240 lines
19 KiB
Markdown
# CODE-REVIEW-376-r1
|
||
|
||
Issue: #376 — «Пачка Low из adversarial-аудита beta.4» (лёгкий трек, редакция 2)
|
||
Ветка: `issue/376-audit-lows-beta4`, база — актуальный `origin/dev`
|
||
Коммиты: `db7f0587` (fix, User-Visible: yes) + `dceaf2d8` (build: refresh bundle trees, User-Visible: no)
|
||
Заход: r1 · этап code · блокирующих циклов израсходовано 0 из 2 (лёгкий трек)
|
||
|
||
## 1. Скоуп
|
||
|
||
ТЗ (редакция 2, принята зелёным SPEC-REVIEW-376-r2) содержит пять точечных
|
||
правок; пункт (в) вынесен в #377 на полный трек и в этот код-ревью не входит:
|
||
|
||
- **(а)** `houseplan-space-card`: `title: null` (YAML `title:` без значения)
|
||
получает тот же компактный кадр, что и `title: ''`.
|
||
- **(б)** USER-GUIDE(.ru), раздел «Редактор подложки»: зафиксировано, что
|
||
маркеры устройств и подписи комнат не перехватывают указатель. Код не
|
||
трогается — это осознанно существующее поведение.
|
||
- **(г)** `furnitureScreenScale` в labs-изометрии больше не применяет
|
||
2D-формулу компенсации камеры; в iso — `1`, как у обычного декора.
|
||
- **(д)** `docs/TESTING.md:1704` — оговорка про `light_pools` opt-in (#374).
|
||
- **(е)** `space-card.ts`: dispose-гейт `light_pools` выровнен на `!== true`,
|
||
зеркально строгому рендер-гейту `=== true`.
|
||
|
||
Диапазон разбора: `git log --oneline origin/dev..HEAD` (2 коммита),
|
||
`git diff origin/dev...HEAD` (40 файлов: 2 файла продукта класс A
|
||
(`src/houseplan-card.ts`, `src/space-card.ts`), тесты/скрипты класс B
|
||
(`demo/smoke_space_card.mjs`, `test/*.mjs`, `scripts/mutation-gate.mjs`),
|
||
документация класс C (`CHANGELOG*`, `USER-GUIDE*`, `TESTING.md`,
|
||
`docs/images/**`), сгенерированный бандл класс D (`dist/**`,
|
||
`custom_components/houseplan/frontend/**`)).
|
||
|
||
Первый заход код-ревью для этого issue — раздел «унаследовано» не нужен, все
|
||
пять пунктов разобраны заново по коду и по AC ТЗ rev2.
|
||
|
||
## 2. Как проверялось
|
||
|
||
### Построчная сверка кода против ТЗ и AC
|
||
|
||
- **AC-а** (`src/space-card.ts:826`): `compactTopFrame: this._config.title === '' || this._config.title === null` —
|
||
дословно как в ТЗ. `title` для рендера заголовка (`:816`)
|
||
`this._config.title !== undefined ? this._config.title : sp?.title || ''`
|
||
— `null !== undefined`, так что `title = null`, и `${title ? … : nothing}`
|
||
(:875) уже скрывал header для `null` до этой правки; правка меняет только
|
||
кадр — согласуется с утверждением ТЗ «скрытие header уже работает».
|
||
`undefined`-ветка (дефолтный заголовок) не тронута.
|
||
- **AC-б**: код `plan.styles.ts` не менялся (проверено — файла нет в диффе).
|
||
Утверждение ТЗ и текста доков проверено по коду: `.stage.mode-decor
|
||
.devlayer, .stage.mode-decor .devlayer *, .stage.mode-decor .dev::before`
|
||
(`plan.styles.ts:863-865`) — `devlayer` действительно является родителем
|
||
разметки room-label (`houseplan-card.ts:11069`, CSS-переменные
|
||
`--rl-icon-size`/`--rl-font` заданы на том же `<div class="devlayer">`, что
|
||
оборачивает и устройства, и лейблы), так что универсальный потомок `*`
|
||
накрывает и лейблы. Формулировка в `USER-GUIDE.md:694-696` и
|
||
`USER-GUIDE.ru.md:1242-1244` («device markers and room labels do not
|
||
intercept the pointer» / «маркеры устройств и подписи комнат не
|
||
перехватывают указатель») — не догадка, а точное описание существующего
|
||
кода.
|
||
- **AC-г** (`src/houseplan-card.ts:8089`): `const furnitureScreenScale =
|
||
this._renderProjection === 'iso' ? 1 : furniturePlanScreenScale(...)`.
|
||
Прослежен весь путь потребления значения: единственный вызов
|
||
`furniturePlanScreenScale(` во всём `_renderDecorLayer` (:8074-8209, тест
|
||
`furniture-stroke-contract.test.mjs` считает вхождения — единственный
|
||
источник для декор-слоя), проброс в `furnitureStrokePx(strokeWidth,
|
||
furnitureScreenScale)` (:8157) и в `_renderFurniturePlacementPreview(
|
||
furnitureScreenScale)` (:8209) — общий и превью, и сохранённые фигуры берут
|
||
одно и то же число. `_renderDecorLayer()` вызывается безусловно (единственный
|
||
сайт — `:10992`, гейт только `hideDecor`, не проекция), то есть декор,
|
||
включая мебель, рендерится и в iso — значит правка реально исполняется в
|
||
iso-режиме, а не мертвый код. `_renderWallBodies`/`_renderIsoWalls` (:8771,
|
||
:8888) — гейты проекции на стены, к декору не относятся, не спутаны.
|
||
- **AC-д** (`docs/TESTING.md:1705`): `unless \`light_pools: true\` opts them
|
||
in (#374)` — добавлено ровно к пункту про static room cards, дословно как
|
||
в AC-д (grep подтверждён).
|
||
- **AC-е** (`src/space-card.ts:290-297, 847`): dispose-гейт
|
||
`this._config.light_pools !== true` — точное зеркало рендер-гейта
|
||
`lightPools: this._config.light_pools === true` (:847). `light_pools: 1`
|
||
теперь и не рисует пулы (уже было так, `===true` не давал `1`), и не
|
||
сохраняет рантайм живым (раньше `!this._config.light_pools` было `false`
|
||
для `1`, рантайм не dispose'ился — асимметрия, которую чинит эта правка).
|
||
|
||
### Гейты — прогнаны лично на этом SHA (зелёного Validate для `dceaf2d8` не найдено)
|
||
|
||
| Гейт | Результат |
|
||
|---|---|
|
||
| `npx tsc --noEmit` | чисто, без вывода |
|
||
| `npm test` | **1551 pass / 0 fail / 1 skip** (1552 всего) — совпадает с заявленным автором числом |
|
||
| `npm run build` | сборка чистая, `git status` после — пусто (бандл в `dist/` уже байт-в-байт совпадает с закоммиченным) |
|
||
| `npm run bundle:sync` | без диффа (три копии дерева уже синхронны) |
|
||
| `npm run bundle:budget` | initial View **275801 B** / 300000 (headroom 24199) — совпадает с числом автора |
|
||
| `node scripts/check-docs.mjs` | **passed (7 files, 10 external links)** — обязателен, диф трогает `src/**` |
|
||
|
||
Мутанты (дишонести-дисциплина: тест обязан уметь падать) — прогнаны лично,
|
||
не только по слову автора:
|
||
|
||
```
|
||
node scripts/mutation-gate.mjs --id=space-card-null-title-compact-narrowed
|
||
→ поймано 1 из 1 (test/space-card-audit-lows.test.mjs краснеет без правки)
|
||
node scripts/mutation-gate.mjs --id=furniture-stroke-iso-camera-mismatch
|
||
→ поймано 1 из 1 (test/furniture-stroke-contract.test.mjs краснеет без правки)
|
||
```
|
||
|
||
(Полный прогон `mutation-gate.mjs` без `--id` пробовал ошибочно как общий
|
||
чек — упал на несвязанной строке `tests_backend/test_frontend_assets.py`
|
||
из-за отсутствия `pytest` в окружении; это существующая инфраструктурная
|
||
дыра окружения, не относится к диффу #376 — `custom_components/**/*.py` и
|
||
`tests_backend/**` в диффе нет, так что backend-гейт для этой задачи не
|
||
обязателен и не разбирался дальше.)
|
||
|
||
### Смоки — выбор обоснован, не «прогнал всё»
|
||
|
||
`node scripts/smoke-select.mjs --base origin/dev --head HEAD`: изменено 2
|
||
файла `src/**`, 3 символа на изменённых строках. Инструмент не нашёл ни
|
||
одного смока с прямым совпадением по символу; 19 файлов помечены как слабая
|
||
связь через одно распространённое имя `_config` (общее для всех карточек,
|
||
не специфично для этой правки) — решение ревьюера: не прогонять, связь
|
||
неинформативна. Неопределённость по `_renderProjection`,
|
||
`furniturePlanScreenScale` — новых смоков под них нет; решаю по AC и по
|
||
доказательству чтением (см. AC-г выше) плюс по одному прогнанному смоку,
|
||
который фактически упражняет соседний код того же слоя:
|
||
|
||
- `demo/smoke_space_card.mjs` — **обновлён самим диффом** (добавлена ветка
|
||
`title: null`), прямое доказательство AC-а исполнением: `nullTitleFrame`
|
||
байт-в-байт равен `compactFrame` (`{x:-50,y:100,w:1100,h:850}`),
|
||
`nullTitleHasTitle:false`. Прогнан — **OK**.
|
||
- `demo/smoke_glow_blending.mjs` — регистрированная связь: явно создаёт
|
||
`houseplan-space-card` с `light_pools: true` и без. Прогнан —
|
||
`{"ok":true,"blend":"screen","pools":60,"staticParity":true,
|
||
"staticPools":60}` — не деградировал (та же цифра, что в отчёте автора).
|
||
Не покрывает конкретно новую ветку «`light_pools: 1`» (её ловит только
|
||
новый юнит-тест) — рантайм-регрессию по `true`/`false` парности проверяет.
|
||
- `demo/smoke_furniture.mjs` — регистрированная связь (общий декор/мебель
|
||
слой). Прогнан — все 18 полей `true`/OK, включая
|
||
`furnitureFollowsPhysicalCameraZoom`, `designerAndPrimitiveMatchOrdinaryDecor`
|
||
— не деградировал. Не покрывает iso-ветку напрямую (в фикстуре нет
|
||
`projection: 'iso'`), только соседнее 2D-поведение.
|
||
|
||
Остальные 200 смоков **не прогонялись** — ни прямой, ни регистрированной
|
||
связи с изменёнными тремя символами инструмент не нашёл, а полный прогон
|
||
матрицы — предрелизная обязанность, не гейт этого ревью.
|
||
|
||
### Golden — сознательно не прогонялся, обоснование
|
||
|
||
Diff меняет видимый рендер (кадр space-card, толщина штриха мебели в iso),
|
||
что по правилам требует рассмотреть `golden:verify`. Проверено по
|
||
`demo/golden/matrix.mjs` и `demo/golden/harness.mjs`, что ни один текущий
|
||
golden-сценарий не упражняет ни одну из двух изменённых веток:
|
||
|
||
- **space-card** нигде не участвует в golden-матрице (нет ни одного
|
||
сценария с `houseplan-space-card`) — `golden:verify` физически не может
|
||
ни поймать, ни подтвердить AC-а.
|
||
- **iso + мебель**: все `projection: 'iso'` сценарии (`isometric-*`,
|
||
7 штук) используют `space: 'golden-geometry' | 'golden-lighting' |
|
||
'golden-wall-junctions' | 'perf-floor-2' | 'golden-opening-symbols'` без
|
||
`decorOverride`; дефолтный `space.decor = []` (`harness.mjs:245,514`), то
|
||
есть в них нет ни одной декор-фигуры вообще. Сценарии с мебелью
|
||
(`golden-decor-sofa` через `decorOverride: decorLayerFixture`) — только в
|
||
`mode: 'decor'`/`mode: 'view'` без `projection: 'iso'` (`furniture-*`,
|
||
`decor-over-opaque-hover-light`, `decor-over-glow-base-dark`) — там
|
||
`furnitureScreenScale` идёт по прежней 2D-формуле, не тронутой этой
|
||
правкой.
|
||
|
||
Прогон `golden:verify` дал бы нулевую диагностическую ценность для этого
|
||
диффа — решение не прогонять, а не пропуск.
|
||
|
||
### Invariants / backend
|
||
|
||
Diff не трогает геометрию комнат, толщину стен, `layout`, `marker.space`,
|
||
`open_spans`, `custom_components/**/*.py` — `npm run invariants` и
|
||
`pytest tests_backend` не требуются и не запускались.
|
||
|
||
### Трейлеры и changelog
|
||
|
||
`db7f0587`: `Issue: #376`, `User-Visible: yes`, оба changelog
|
||
(`docs/CHANGELOG.md`, `docs/CHANGELOG.ru.md`) правлены в этом же коммите —
|
||
верно. `dceaf2d8`: `Issue: #376`, `User-Visible: no` — верно для
|
||
build-коммита (только бандл-деревья).
|
||
|
||
### Одно число — один источник
|
||
|
||
Единственная новая пользовательская величина в этом диффе —
|
||
`furnitureScreenScale` для iso. Проверено: значение вычисляется один раз в
|
||
`_renderDecorLayer` и передаётся обоим потребителям (сохранённые фигуры и
|
||
placement-preview) одним и тем же параметром — дублирования источника нет,
|
||
и именно это утверждение теперь явно проверяет
|
||
`furniture-stroke-contract.test.mjs` («the viewport scale is resolved once
|
||
for the whole decor layer»). `compactTopFrame`/`lightPools` — булевы гейты
|
||
без дублирующего представления величины, вопрос неприменим.
|
||
|
||
## 3. Что проверено и корректно
|
||
|
||
- Все пять пунктов (а, б, г, д, е) реализованы ровно так, как описаны в
|
||
принятом ТЗ rev2, без расширения скоупа и без пункта (в) — вынесен в #377.
|
||
- `title: null` даёт компактный кадр байт-в-байт равный `title: ''`,
|
||
`undefined`-ветка (дефолтный заголовок) не задета — подтверждено
|
||
исполнением смока и юнит-тестом с мутантом.
|
||
- iso-ветка мебели отключает 2D-компенсацию, не затрагивая ни один текущий
|
||
golden/смок-сценарий (все они либо без мебели в iso, либо без iso для
|
||
мебели) — подтверждено чтением плюс мутантом.
|
||
- `light_pools` render/dispose гейты симметричны — подтверждено чтением,
|
||
юнит-тестом (grep-контракт с явным запретом старого гейта) и смоком на
|
||
`true`/`false` параx.
|
||
- Документация (б, д) точно описывает существующий код, не выдаёт догадку
|
||
за факт — сверено построчно с CSS/JS.
|
||
- Трейлеры, оба changelog, класс изменений (A: `src/*.ts`; B: тесты/скрипты;
|
||
C: доки; D: бандл) — все соблюдены.
|
||
- Бюджет бандла практически не изменился (+3 Б), докстрока фингерпринта
|
||
скриншотов синхронна с текущим `src/**`.
|
||
|
||
## 4. Чего не проверял (и почему)
|
||
|
||
- Полный смок-набор (203 файла) — не прогнан; `scripts/smoke-select.mjs` не
|
||
нашёл ни прямой, ни сильной регистрированной связи, кроме уже прогнанных
|
||
трёх. Это предрелизная обязанность, не гейт код-ревью соразмерного диффа.
|
||
- `npm run golden:verify` — не прогнан, обоснование в §2 (ни один сценарий
|
||
не упражняет изменённые ветки; см. выше).
|
||
- `python -m pytest tests_backend` — не прогнан, диф не трогает
|
||
`custom_components/**/*.py`.
|
||
- `npm run invariants` — не прогнан, диф не трогает геометрию/толщину/layout.
|
||
- Полный `mutation-gate.mjs` без `--id` — не завершён (упал на
|
||
несвязанном backend-мутанте из-за отсутствия `pytest` в окружении
|
||
ревьюера); не относится к диффу, не пересматривался повторно ради этого.
|
||
- Ручного браузерного тестирования не было (по правилам цикла) — заменено
|
||
прогоном названных смоков и построчным чтением кода.
|
||
|
||
## 5. Находки
|
||
|
||
Нет ни одной High, ни одной Medium (в скоупе или вне). Все три находки
|
||
предыдущего (spec) раунда были закрыты в редакции 2 и здесь не
|
||
пересматриваются повторно — это код-ревью первого захода для этапа `code`,
|
||
делить по дельте не от чего.
|
||
|
||
## Вердикт
|
||
|
||
Все пять AC (а, б, г, д, е) выполнены и доказаны — частью исполнением
|
||
(смоки, мутанты), частью чтением с явной пометкой. Гейты, соразмерные
|
||
диффу, зелёные. Golden и backend осознанно не прогонялись с указанной
|
||
причиной. Возражений по скоупу, процессу или качеству нет.
|
||
|
||
**Зелёный.**
|