Files
houseplan-card/docs/reviews/CODE-REVIEW-474-r1.md
T
claude[bot] c82ba84cc7
Проверка (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
docs: review document for #474
Issue: #474
User-Visible: no
2026-09-06 13:38:29 +00:00

184 lines
17 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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
```