docs: review document for #376

Issue: #376
User-Visible: no
This commit is contained in:
claude[bot]
2026-08-29 21:55:13 +03:00
committed by Codex
parent 2e32cbcb38
commit 2775a7032a
+239
View File
@@ -0,0 +1,239 @@
# 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 осознанно не прогонялись с указанной
причиной. Возражений по скоупу, процессу или качеству нет.
**Зелёный.**