# 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` заданы на том же `
`, что оборачивает и устройства, и лейблы), так что универсальный потомок `*` накрывает и лейблы. Формулировка в `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 осознанно не прогонялись с указанной причиной. Возражений по скоупу, процессу или качеству нет. **Зелёный.**