docs: review document for #474
Проверка (CI) / Классификация изменённых файлов (push) Successful in 1m46s
Проверка (CI) / Предполётные проверки: документация, провенанс, процесс (push) Failing after 2m20s
Проверка (CI) / Переиспользование: это дерево уже проверено (push) Successful in 1m3s
Проверка (CI) / HACS: валидация репозитория (push) Failing after 23s
Проверка (CI) / Hassfest: манифест интеграции (push) Failing after 34s
Проверка (CI) / Фронтенд: типы, юниты, мутанты, синхрон бандла (push) Failing after 16m17s
Проверка (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) Failing after 9m53s

Issue: #474
User-Visible: no
This commit is contained in:
claude[bot]
2026-09-06 13:38:29 +00:00
parent 605a9a1c78
commit c82ba84cc7
+183
View File
@@ -0,0 +1,183 @@
# CODE-REVIEW-474-r1
Issue: [#474](https://github.com/Matysh/houseplan-card/issues/474) — «Стартовый граф: арт мебели уходит в ленивый чанк»
Этап: code · заход r1 · блокирующих циклов израсходовано 0 из 4
Ветка: `issue/474-lazy-furniture-art`, HEAD `605a9a1c` (после ребейза на `origin/dev` = `ab01586b`)
Спецификация принята зелёным на r3 (`docs/reviews/SPEC-REVIEW-474-r3.md`); это первый заход кодового ревью — авторская попытка ранее вернулась не по коду, а по конфликту ребейза (`docs/specs/README.md`), решённому без содержательных изменений реализации, поэтому разбор полный, не дельта.
## Скоуп
Диапазон `git diff origin/dev...HEAD` (62 файла): разрез генератора мебели на
каталог (eager) и арт (ленивый чанк), новый `FurnitureArtRuntime`
(`src/furniture-art-runtime.ts`), три точки готовности арта (приём конфига,
редакторный `adopt`, бут-гейт), перевод магнита `furniture-placement.ts` на
каталог, бюджет бандла (потолок 300 500 → 290 500), семь мутантов-свидетелей,
новый смок `smoke_furniture_lazy_art`, unit-тесты, `golden/harness.mjs`-гейт на
`.dfurn`, i18n-тост в четырёх языках, `docs/ARCHITECTURE.md`/`docs/FURNITURE.md`.
Не в скоупе диффа и не проверялось повторно: сам арт/каталог мебели (не
менялся), редактор мебели вне точки `adopt`, бэкенд (`.py` не тронут).
## Как проверялось
Гейты, прогнанные лично на этом SHA (список честности):
| Гейт | Команда | Результат |
|---|---|---|
| Типы | `npx tsc --noEmit` | зелёный |
| Unit | `npm test` | 2139 тестов, 2138 pass, 1 skipped (пред-существующий), 0 fail |
| Сборка | `npm run build` | зелёная, tsc+rollup |
| Бюджет бандла | `node scripts/bundle-budget.mjs` | initial View 289 760 Б (потолок 290 500±2000, стена 301 066, запас 11 306 Б); предупреждение о низком запасе (#367) — известный, не этой задачи долг |
| Дерево бандла | `npm run bundle:sync` + `git status` | пусто — committed `dist/`, `custom_components/.../frontend`, `demo/srv/assets` побайтово совпадают со свежей сборкой |
| Документация | `node scripts/check-docs.mjs` | зелёный, 7 файлов, отпечаток скриншотов свежий |
| Смок задачи | `node demo/smoke_furniture_lazy_art.mjs` | OK, все 15 проверок (AC2–AC5, AC11) |
| Смоки, названные в AC7 | `smoke_furniture`, `smoke_furniture_polish`, `smoke_decor`, `smoke_decor_default_persist`, `smoke_lazy_editor_chunk`, `smoke_preloader_lifecycle` | все OK |
| `smoke-select.mjs --base origin/dev --head HEAD` | инструмент отбора | 9 прямых совпадений — все на `_showToast` (общее имя, слабая связь: свежий вызов composeUnsub — аддитивный, реализацию `_showToast` не трогает — не гонял все девять); 4 «зарегистрированные связи» — `EditorRuntimeLoader`/`LanguageRuntime` не изменялись в этом диффе, `smoke_lazy_editor_chunk` уже прогнан по AC7, остальные три (`smoke_entry_stale`, `smoke_french_locale`, `smoke_german_locale`) не гонял — связь по аналогии контракта, не по изменённому модулю |
| Мутанты §4.6 (7 шт.) | `node scripts/mutation-gate.mjs --id=<each>` по одному (флаг `--id=` берёт первое вхождение) | все семь: «поймано 1 из 1» — `furniture-art-eager-import`, `furniture-art-fallback-never-settles`, `furniture-art-no-retry-nonce`, `furniture-art-boot-gate-ignored`, `furniture-placement-needs-art`, `furniture-art-fingerprint-unchecked`, `furniture-art-editor-adopt-skipped` |
| Golden | `npm run golden:verify` (полный набор — diff меняет рендер мебели и бут-гейт) | 15 сцен «different», identical к прогону на чистом `origin/dev` в отдельном worktree (см. ниже) — 0 расхождений от диффа; все `furniture-*`/`decor-*`-мебельные сцены — `passed` |
| Инварианты модели | не гонял | diff не трогает рёбра комнат, записи толщины, `layout`, `marker.space`, `open_spans` — магнит мебели проверяет только каталог размеров, геометрию стен не меняет |
| `pytest tests_backend` | не гонял | `custom_components/**/*.py` не тронут |
| `npm run inventory` | прогнан | 2138 unit / 310 pure-backend / 211 HA-harness / 227 browser smokes — числа не копировал вручную |
### Golden: изоляция пред-существующего дрейфа
`golden:verify` не даёт прогон по одному сценарию (`policy.mjs` требует полный
набор для verify), поэтому гонял целиком. Список из 15 «different» —
исключительно `isometric-*` (10) и `*-color-popover-*` (5) — не пересекается с
темой задачи. Чтобы не приписать их этому диффу, поднял `origin/dev`
(`ab01586b`) в отдельном `git worktree`, собрал и прогнал тот же
`golden:verify`: результат — те же самые 15 сцен «different», побуквенно то же
множество id. Дрейф пред-существующий (среда/Chromium), не regressии.
Worktree убран (`git worktree remove --force`).
## Находки
### Low — `scripts/bundle-budget.mjs`: печать не называет новый ленивый граф
Файл: `scripts/bundle-budget.mjs`, функция сборки `lines` для CLI-вывода и
блок `GITHUB_STEP_SUMMARY` (около строк 322–344).
Манифест и `assertBundleBudget` уже возвращают `lazyFurnitureArtGzipBytes`
(добавлено этим диффом, покрыто тестом в `test/bundle-assets.test.mjs`), но
печатаемые строки CLI перечисляют только `lazy editor`/`lazy locale`/
`lazy isometric` — новый граф мебели не появляется ни в консоли, ни в таблице
`GITHUB_STEP_SUMMARY`, хотя структурно стоит рядом с остальными тремя.
**Сценарий отказа:** это не влияет на корректность гейта — `assertBundleBudget`
по-прежнему бросает исключение, если `lazyFurnitureArtFiles` пуст или
пересекается с initial (проверено чтением и тестом). Cтрадает только
наблюдаемость: инженер, читающий вывод `bundle:budget` в CI-сводке при
следующей правке бюджета, не увидит вес чанка мебели рядом с остальными тремя
графами и должен будет открыть JSON-манифест вручную.
Не блокирует; можно поправить в этой же задаче (одна строка в `lines` и в
таблице `GITHUB_STEP_SUMMARY`) или снять с явной пометкой «не печатаем — граф
редко меняется» — оставляю решение автору.
## Что проверено и корректно
- **Разрез генератора (§4.1).** `scripts/generate-furniture-assets.mjs` пишет
`furniture-plan-catalog.generated.ts` (eager, id/группа/категория/размеры) и
`furniture-plan-art.generated.ts` (`GENERATED_FURNITURE_ART` + отпечаток),
импортируемый только динамически из `furniture-art-runtime.ts` и статически
из `houseplan-editor-runtime.ts`. Инвариант «id каталога == ключи арта» —
тест в `test/furniture-assets.test.mjs`, проверен запуском.
- **`FurnitureArtRuntime` (§4.2).** Контракт `ready|pending|fallback` зеркалит
`LanguageRuntime`: `art()` синхронный и не запускает загрузку, `ensure()`
идемпотентен (один in-flight промис — тест «only a single in-flight load»),
`adopt()` синхронный и терминально отвергает чужой отпечаток, `fallback`
осевший (второй провал не откатывает раньше `_failed`, не ретраится дальше —
проверено и мутантом, и unit-тестом). Нонс на attempt 1 — через
content-hashed токен `bundle-manifest.mjs` подставляет так же, как для
`de`/`fr`/`iso` (проверено чтением диффа `bundle-manifest.mjs` — счётчик
замен теперь `1/1/1/1/1/1`, шестой — новый).
- **Три точки готовности (§4.3).** Приём конфига — `ensureFurnitureArtFor` в
`_seedDecorStyle`, вызывается на всех путях, где в `_serverCfg` может
появиться новая мебель: первичный кэш (`LS_CFG`), `_adoptStructuralResponses`
(оба live-sync входа — `_syncLiveConfig`-подобные вызовы на строках 4314 и
4491 — заведены через один и тот же метод). Пути, где `_serverCfg`
переписывается напрямую (`_setAreaLifecycleConfig`, `_syncNewDevices`,
`_ackNewDevice`, `_writeConfig`'s canonicalize) намеренно не вызывают
`_seedDecorStyle` — они правят только `settings`/`marker_area_snapshot`, не
`spaces[].decor`, проверено чтением каждого из четырёх сайтов. Редактор —
статический импорт + `FURNITURE_ART_RUNTIME.adopt(...)` на уровне модуля
`houseplan-editor-runtime.ts`, которое само является лениво импортируемым
корнем (`houseplan-card.ts:697`), то есть `adopt` гарантированно выполняется
до любого рендера палитры/призрака. Бут-гейт — `furnitureArtBootPending` в
условии `_bootWatch`, `BOOT_MAX_MS` остаётся безусловным потолком (проверено
чтением: `elapsed >= BOOT_MAX_MS ||` стоит первым слагаемым `||`).
- **Магнит (AC6).** `furniture-placement.ts` проверяет `furnitureSymbol`
(каталог), не `furnitureGraphic` (арт) — размещение designer-предмета
работает в `pending`, подтверждено unit-тестом и мутантом
`furniture-placement-needs-art`.
- **Отказ (§4.4, AC4/AC5).** Смоком подтверждено: два провала → `fallback`,
вуаль снята, устройства живы, только legacy-предмет рисуется, ровно один
тост; чужой отпечаток → терминальный `fallback` за один запрос без ретрая.
- **Бюджет (AC1).** Измерено лично: initial View 289 760 Б, потолок 290 500,
стена не тронута, `lazyFurnitureArtFiles` непуст и не пересекается с initial
— `assertBundleBudget` бросает при нарушении (проверено мутантом
`furniture-art-eager-import`).
- **Golden (AC8).** Гейт в `demo/golden/harness.mjs` требует все
`[data-kind="furniture"]` активного пространства после снятия вуали — если
бы фолбэк не различался от пустого кадра, гейт сам бросил бы; полный
`golden:verify` подтвердил 0 диффа от задачи (см. раздел про изоляцию выше).
- **Мутанты (AC9).** Семь строк в §4.6 = семь id в `mutation-gate.mjs` = семь
«поймано 1 из 1» при личном прогоне каждого по отдельности.
- **Ядро (AC10).** `houseplan-card.ts` — 13 656 строк, тест
`core-file-budget.test.mjs` зелёный (потолок не превышен).
- **i18n.** `toast.furniture_art_load_failed` — во всех четырёх словарях
(`en`/`ru`/`de`/`fr`), формулировка по образцу `toast.locale_load_failed`.
- **Трейлеры.** Все три продуктовых коммита несут `Issue: #474` и
`User-Visible: no`; changelog не тронут — соответствует §7 спецификации
(визуальных изменений нет).
- **Документация.** `docs/ARCHITECTURE.md` и `docs/FURNITURE.md` описывают
новую ленивую границу; отпечаток скриншотов обновлён в том же коммите
(605a9a1c), `check-docs.mjs` зелёный.
- **Единственный источник числа.** Диф не добавляет и не меняет ни одной
видимой пользователю величины (UX не меняется по AC2/§6.1) — гейт
`single-source-numbers.test.mjs` прогнан в составе `npm test`, не задет.
## Чего не проверял
- `python -m pytest tests_backend` — бэкенд не тронут этим диффом.
- `npm run invariants` — диф не касается геометрии стен/комнат/`layout`;
единственная геометрическая точка соприкосновения (`furniture-placement.ts`)
теперь читает только каталог размеров, ключи решётки не затронуты.
- Три «зарегистрированные связи» инструмента отбора смоков —
`smoke_entry_stale`, `smoke_french_locale`, `smoke_german_locale` — оставлены
непрогнанными: связь идёт по аналогии архитектурного контракта
(`EditorRuntimeLoader`/`LanguageRuntime`), а не по изменённому файлу; ни
`editor-runtime-loader.ts`, ни `language-runtime.ts` в диффе нет.
- Девять «прямых совпадений» на `_showToast` — общее имя, использованное по
всей карточке; прогнан только тематически релевантный (входит в
AC7-список), остальные восемь не гонял — правка добавляет вызов
`composeUnsub`, не трогая саму `_showToast`.
- Performance-профили — не названы в AC и не задет чувствительный к перфу
путь (бут-гейт ограничен тем же `BOOT_MAX_MS`, что и раньше).
- Ручное открытие карточки в браузере вне смоков/golden — не делал; браузерные
смоки и golden воспроизводят все сценарии AC через настоящий Chromium.
## Вердикт
Все 11 AC доказаны исполняемыми гейтами, которые я прогнал лично и которые
умеют падать (проверено мутантами на защитных контрактах и сверкой golden
против чистого `dev` для исключения пред-существующего дрейфа). Единственная
находка — Low, чисто наблюдательная, не по AC и не по корректности.
**Зелёный.**
---
<!-- material-anchors: сгенерировано конвейером (#414) -->
## Материал раунда
- Ветка: `issue/474-lazy-furniture-art`, коммит `605a9a1c786c` — ребейз его осиротит, и это нормально: ниже якоря, которые ребейз не меняет.
- Дерево материала: `780f546ed4d2a9be72796f222d3d8bb0331c0268`
```
git log --all --format='%H %T' | grep 780f546ed4d2
```
- ТЗ `docs/specs/474-lazy-furniture-art.md`, блоб `b4e8a03e921dd45220d33cd55afe95fd60440bbe`
```
git log --all --find-object=b4e8a03e921dd45220d33cd55afe95fd60440bbe -- docs/specs/474-lazy-furniture-art.md
```