mirror of
https://github.com/Matysh/houseplan-card
synced 2026-10-02 04:38:55 +00:00
docs: spec #474 — арт мебели в ленивый чанк
Issue: #474 User-Visible: no
This commit is contained in:
Executable
+227
@@ -0,0 +1,227 @@
|
||||
# #474 — Стартовый граф: арт мебели уходит в ленивый чанк
|
||||
|
||||
- **Issue:** https://github.com/Matysh/houseplan-card/issues/474
|
||||
- **Тип / приоритет:** infra, tech-debt / P2
|
||||
- **Трек:** полный — путь отрисовки View, новая ленивая граница, бут-вуаль
|
||||
- **Оценка:** ценность 7/10 (единственный рычаг размера, не требующий решения по #367); сложность 4/10; риск 3/10 (класс #352–#355)
|
||||
- **Связано:** #367 (закрыт рекалибровкой), #352–#355 (ленивые модули и их отказы), #438 (храповик потолка), #451 (вынос спасает ядро), #425 (потолок ядер)
|
||||
|
||||
## 1. Проблема
|
||||
|
||||
Стартовый граф View — 299 658 Б gzip при стене 301 066: запас 1 408 Б, то есть
|
||||
ноль. Три релиза подряд ответом на упор в границу был перенос границы
|
||||
(аудиты 05.09 и 06.09, M1). Решение по #367 «резать монолит» не принято.
|
||||
|
||||
Внутри монолита есть один кусок, который платит каждый план, а нужен только
|
||||
планам с мебелью: `src/furniture.ts` статически импортирует
|
||||
`GENERATED_FURNITURE_PLAN` из `furniture-plan-art.generated.ts` — 44
|
||||
дизайнерских символа с SVG-путями (`art.d`, 31 716 знаков). Замер на дереве
|
||||
(S2, `90b185f6`): без `art.d` initial View = **289 062 Б, −10 596 Б**. Меню-арт
|
||||
палитры уже ленивый (его импортирует только редактор); арт плана — нет, потому
|
||||
что View рисует предметы сам.
|
||||
|
||||
## 1.1. Сценарий
|
||||
|
||||
Персона — владелец плана в HA, открывает вкладку с карточкой на настенном
|
||||
планшете или в телефоне. У большинства планов мебели нет; у тех, где есть,
|
||||
предметы видны с первого кадра.
|
||||
|
||||
## 1.2. Что человек увидит до и после
|
||||
|
||||
До: ничего не видно — 10,6 КБ арта грузятся всегда, вместе с монолитом.
|
||||
|
||||
После: **визуально ничего не меняется.** План без мебели не грузит арт вовсе.
|
||||
План с мебелью запускает загрузку при приёме конфига, до первого кадра, и
|
||||
бут-вуаль первого открытия держится, пока арт не осел, — предметы появляются
|
||||
вместе с планом, а не всплывают позже. Если чанк не загрузился (сеть, чужая
|
||||
сборка во вкладке), предметы рисуются как неизвестные символы — ничего, —
|
||||
и один раз показывается тост о недоступной части карточки; план, устройства и
|
||||
редактор работают. Для инженера: initial View ≈ 289 КБ, потолок-храповик
|
||||
опускается, запас до стены ≈ 12 КБ.
|
||||
|
||||
## 2. Скоуп
|
||||
|
||||
1. Разрез библиотеки мебели: **каталог** (id, группа, категория, w, h, и
|
||||
примитивная геометрия 12 legacy-символов) остаётся в стартовом графе;
|
||||
**арт** 44 дизайнерских символов уходит в ленивый чанк.
|
||||
2. `FurnitureArtRuntime` — page-scoped загрузчик по контракту `LanguageRuntime`
|
||||
(§4).
|
||||
3. Точка запуска при приёме конфига и ожидание в буте (§4.3).
|
||||
4. Бюджет бандла: ленивый чанк арта — отдельная роль в манифесте; потолок
|
||||
initial View опускается храповиком (§4.5).
|
||||
5. Доказательства: unit на рантайм и разрез, мутанты на защитные контракты,
|
||||
смоки мебели и golden ждут `ready`.
|
||||
|
||||
## 3. Не-скоуп
|
||||
|
||||
- Изменение самого арта, каталога, размеров по умолчанию, палитры редактора.
|
||||
- Другие кандидаты на вынос (варианты B и C из issue: словарь `en`, стили,
|
||||
топология) — отдельными задачами после этой.
|
||||
- Пересмотр стены `INITIAL_VIEW_GZIP_BUDGET`: стена не трогается, опускается
|
||||
только потолок-храповик по правилу #438.
|
||||
- Ленивая загрузка *по символу* (44 чанка): один чанк, один запрос.
|
||||
- `houseplan-space-card`: статическая карточка пространства не рисует декор
|
||||
сама (см. `smoke_space_card_decor_capability`); если рисует — она проходит
|
||||
через тот же `furnitureGraphic` и получает поведение бесплатно, отдельного
|
||||
контракта нет.
|
||||
|
||||
## 4. Контракт
|
||||
|
||||
### 4.1. Разрез генератора
|
||||
|
||||
`scripts/generate-furniture-assets.mjs` пишет два файла вместо одного:
|
||||
|
||||
- `src/furniture-plan-catalog.generated.ts` — `GENERATED_FURNITURE_CATALOG:
|
||||
readonly { id, group, category, w, h }[]` (eager);
|
||||
- `src/furniture-plan-art.generated.ts` — `GENERATED_FURNITURE_ART:
|
||||
Readonly<Record<string, { d, viewW, viewH }>>` и
|
||||
`FURNITURE_ART_FINGERPRINT` = отпечаток сборки (как `ISO_SCENE_RUNTIME_FINGERPRINT`),
|
||||
импортируется **только** динамически из `furniture-art-runtime.ts` и
|
||||
статически из редактора (`decor-image-editor.ts` — палитра), который сам
|
||||
ленивый. Rollup сложит оба пути в один общий чанк; манифест обязан видеть
|
||||
его как ленивый (§4.5).
|
||||
|
||||
Инвариант генератора: множество id в каталоге == множество ключей арта; тест
|
||||
читает оба файла и сравнивает.
|
||||
|
||||
### 4.2. `FurnitureArtRuntime` (`src/furniture-art-runtime.ts`)
|
||||
|
||||
Контракт `LanguageRuntime`/`EditorRuntimeLoader`, свёрнутый под одну сущность:
|
||||
|
||||
| член | поведение |
|
||||
|---|---|
|
||||
| `state(): 'ready' \| 'pending' \| 'fallback'` | `pending` до первого `ensure()` и во время загрузки; `fallback` — осевший отказ (оба attempt'а или несовпадение отпечатка) |
|
||||
| `art(id): FurnitureGraphic \| undefined` | синхронно; `undefined` в `pending`/`fallback` |
|
||||
| `ensure(): Promise<void>` | идемпотентно; один in-flight промис; attempt 0 — `import('./furniture-plan-art.generated')`, attempt 1 — тот же URL с нонсом `hp_retry` (Chromium кэширует упавший модуль навсегда, #352–#355); несовпадение `FURNITURE_ART_FINGERPRINT` с `ENTRY_BUILD_FINGERPRINT` — терминальный отказ без повторного импорта |
|
||||
| `onSettled(cb)` | хук для хоста: `requestUpdate()` и одноразовый тост при `fallback` |
|
||||
|
||||
`fallback` — **осевшее** состояние: карточка никогда не остаётся за вуалью
|
||||
из-за арта. Page-scoped синглтон `FURNITURE_ART_RUNTIME`: несколько карточек на
|
||||
дашборде грузят чанк один раз.
|
||||
|
||||
`furnitureGraphic(id)` в `furniture.ts`: legacy-символ → примитивная геометрия
|
||||
как сейчас; дизайнерский → `FURNITURE_ART_RUNTIME.art(id) ?? null`. Неизвестный
|
||||
id → `null` (данные, не сбой — контракт сохраняется). `furniturePathD` —
|
||||
аналогично.
|
||||
|
||||
**`furniture-placement.ts`** сейчас проверяет существование символа через
|
||||
`furnitureGraphic(symbol)`; с ленивым артом это дало бы отказ магнита в
|
||||
`pending`. Проверка переводится на `furnitureSymbol(symbol)` — магниту нужен
|
||||
каталог, не арт. Свидетель обязателен (§4.6).
|
||||
|
||||
### 4.3. Запуск и бут
|
||||
|
||||
- **Приём конфига** (`setConfig`/применение конфига в `houseplan-card.ts`): если
|
||||
в любом пространстве есть `decor[].kind === 'furniture'` с дизайнерским
|
||||
символом — `void FURNITURE_ART_RUNTIME.ensure()`. Проверку «есть ли мебель»
|
||||
делает модуль (`configNeedsFurnitureArt(config)`), ядро — один вызов.
|
||||
- **Бут-вуаль**: условие «бут осел» (`_bootSettle`, ветка
|
||||
`if (!this._booting || this._bootSettling) return;`) дополняется
|
||||
`furnitureArtBootGate(config)`: пока рантайм в `pending` и план нуждается в
|
||||
арте — не оседать. Окно ожидания ограничено самим рантаймом: два attempt'а,
|
||||
дальше `fallback`, и вуаль снимается. Планы без мебели гейт не замечают.
|
||||
- **После бута**: смена конфига (пользователь добавил первый предмет через
|
||||
редактор) → редактор уже импортировал арт статически, чанк в кэше модулей,
|
||||
`ensure()` из приёма конфига разрешается мгновенно; для View без редактора
|
||||
(конфиг обновился с сервера) — `requestUpdate()` из `onSettled`.
|
||||
- **Ядро не растёт**: `houseplan-card.ts` стоит ровно на потолке 13 659
|
||||
(`core-file-budget`); вызовы складываются в существующие строки условий, либо
|
||||
за каждую новую строку выносится строка. Число в дифе.
|
||||
|
||||
### 4.4. Отказ
|
||||
|
||||
`fallback` → предметы = `nothing` (уже штатно для символа из более новой
|
||||
карточки); тост `toast.furniture_art_load_failed` один раз на страницу; в
|
||||
консоли `console.warn` с `safeRuntimeDiagnostic`. При несовпадении отпечатка
|
||||
тост не дублирует плашку согласования версий #462 (`hasCurrentMismatchNotice`
|
||||
— как у редактора).
|
||||
|
||||
### 4.5. Бюджет бандла
|
||||
|
||||
- `scripts/bundle-manifest.mjs`: роль `furniture-art` по модулю
|
||||
`src/furniture-plan-art.generated.ts`, выход `lazyFurnitureArtFiles`.
|
||||
- `scripts/bundle-budget.mjs`: `assertBundleBudget` требует непустой
|
||||
`lazyFurnitureArtFiles`, ни один его файл не в `initialViewFiles`; потолок
|
||||
`INITIAL_VIEW_GZIP_CEILING` опускается по правилу #438 к измеренному факту
|
||||
(ожидаемо ≈ 289 100 → потолок ≈ 289 500); стена не меняется; комментарий про
|
||||
«13,6 КБ» заменяется измеренным.
|
||||
- Три копии бандла пересобираются (fingerprint включает `package.json` — не
|
||||
трогается, но `src/**` меняется).
|
||||
|
||||
### 4.6. Свидетели (мутанты, каждый ловится)
|
||||
|
||||
| id | патч | гард |
|
||||
|---|---|---|
|
||||
| `furniture-art-eager-import` | `furniture.ts` импортирует арт статически | `bundle:budget` (арт в initial) |
|
||||
| `furniture-art-fallback-never-settles` | `fallback` не выставляется после второго attempt'а | unit: после двух отказов `state()==='fallback'`, `ensure()` разрешён |
|
||||
| `furniture-art-no-retry-nonce` | attempt 1 импортирует тот же URL без нонса | unit на loader (как у `de`/`fr`) |
|
||||
| `furniture-art-boot-gate-ignored` | гейт бута не ждёт `pending` | unit `furnitureArtBootGate` / смок первого кадра |
|
||||
| `furniture-placement-needs-art` | магнит снова через `furnitureGraphic` | unit: размещение в `pending` даёт результат |
|
||||
| `furniture-art-fingerprint-unchecked` | рантайм принимает чужой отпечаток | unit |
|
||||
|
||||
## 5. Совместимость и откат
|
||||
|
||||
Конфиг, схема, сохранённые координаты — без изменений. Откат = один revert:
|
||||
статический импорт возвращается, чанк растворяется в монолите, потолок
|
||||
поднимается вручную с объяснением (правило храповика).
|
||||
|
||||
## 6. Критерии приёмки
|
||||
|
||||
| AC | Критерий | Доказательство |
|
||||
|---|---|---|
|
||||
| AC1 | initial View ≤ 290 000 Б gzip; `lazyFurnitureArtFiles` непуст и не пересекается с initial; потолок опущен | `npm run bundle:budget`, число в issue |
|
||||
| AC2 | План с мебелью: предметы в первом показанном кадре (после снятия вуали), без позднего всплытия | смок `smoke_furniture_lazy_art` (новый): мутирует момент, снимает `hpboot`→кадр, `.dfurn` уже есть |
|
||||
| AC3 | План без мебели: чанк арта не запрашивается | тот же смок, сетевые запросы |
|
||||
| AC4 | Отказ загрузки: вуаль снимается, план и устройства живы, предметы = `nothing`, тост один раз | смок с перехватом чанка (`route.abort`) — образец `smoke_lazy_*` #352 |
|
||||
| AC5 | Чужая сборка (отпечаток): терминальный `fallback`, один импорт, без повторов | unit |
|
||||
| AC6 | Магнит к стенам работает в `pending` | unit `furniture-placement` |
|
||||
| AC7 | Редактор: палитра и размещение как прежде | смоки `smoke_furniture`, `smoke_furniture_polish`, `smoke_decor` |
|
||||
| AC8 | Golden: 0 расхождений (сцены с мебелью ждут `ready` в harness) | `golden:verify` |
|
||||
| AC9 | Шесть свидетелей §4.6 в реестре, каждый «поймано 1 из 1» | отрицательные прогоны `--id=` |
|
||||
| AC10 | `houseplan-card.ts` не выше потолка; строки в дифе | `core-file-budget` |
|
||||
| AC11 | Несколько карточек на странице — один запрос чанка | смок AC2 с двумя карточками |
|
||||
|
||||
## 6.1. UX, модель данных, i18n
|
||||
|
||||
UX не меняется (AC2). Модель данных не меняется. i18n: одна строка
|
||||
`toast.furniture_art_load_failed` в en/ru/de/fr («Не удалось загрузить
|
||||
изображения мебели. Обновите страницу.» — по образцу `toast.locale_load_failed`).
|
||||
|
||||
## 6.2. Риски и меры
|
||||
|
||||
| Риск | Мера |
|
||||
|---|---|
|
||||
| Всплытие предметов после первого кадра на медленной сети | гейт бута (§4.3) + смок AC2 с искусственной задержкой чанка |
|
||||
| Rollup вытянет арт в общий чанк с редактором и он не будет «ленивым» с точки зрения манифеста | роль по модулю, не по имени файла; AC1 проверяет по графу |
|
||||
| Chromium кэширует упавший модуль | нонс на attempt 1 (#352–#355), свидетель |
|
||||
| Магнит/резайз мебели зависят от арта | аудит вызовов `furnitureGraphic` (S2: только `houseplan-card.ts:8915` рисует; placement — размеры) + AC6 |
|
||||
| Ядро на потолке | §4.3 «ядро не растёт», AC10 |
|
||||
| Golden-сцены с мебелью станут недетерминированными | harness ждёт `FURNITURE_ART_RUNTIME.state()==='ready'` перед кадром, как ждёт язык |
|
||||
|
||||
## 7. Release-артефакты
|
||||
|
||||
`User-Visible: no`. Changelog не трогается. `docs/ARCHITECTURE.md` — абзац о
|
||||
ленивых границах (арт мебели рядом с языком и изометрией). `docs/FURNITURE.md`
|
||||
— примечание о загрузке арта.
|
||||
|
||||
## 8. Затронутые файлы
|
||||
|
||||
- `scripts/generate-furniture-assets.mjs`, `src/furniture-plan-catalog.generated.ts` (новый), `src/furniture-plan-art.generated.ts`;
|
||||
- `src/furniture-art-runtime.ts` (новый), `src/furniture.ts`, `src/furniture-placement.ts`;
|
||||
- `src/houseplan-card.ts` — два вызова, +0 строк;
|
||||
- `src/decor-image-editor.ts` — импорт арта для палитры;
|
||||
- `src/i18n/*.json` — тост;
|
||||
- `scripts/bundle-manifest.mjs`, `scripts/bundle-budget.mjs`;
|
||||
- `demo/golden/harness.mjs`, `demo/smoke_furniture_lazy_art.mjs` (новый);
|
||||
- `test/furniture-art-runtime.test.mjs` (новый), `test/furniture*.test.mjs`, `test/bundle-budget.test.mjs`;
|
||||
- `scripts/mutation-gate.mjs` — шесть мутантов;
|
||||
- `docs/ARCHITECTURE.md`, `docs/FURNITURE.md`.
|
||||
|
||||
## 9. Принятые предположения
|
||||
|
||||
- Владелец согласился с вариантом A (S2, без возражений до S3).
|
||||
- Rollup при динамическом импорте модуля, который редактор импортирует
|
||||
статически, создаёт общий чанк, достижимый из initial только динамически —
|
||||
это уже так для `iso-scene-render` и словарей.
|
||||
- Замер −10 596 Б снят на `90b185f6`; после #471 и #473 цифра сдвинется на
|
||||
сотни байт, не на килобайты; AC1 фиксирует порог 290 000 с запасом.
|
||||
@@ -202,6 +202,7 @@ GitHub Issues и GitHub Projects (v2) остаются единственным
|
||||
| [#423](https://github.com/Matysh/houseplan-card/issues/423) Полиш support pipeline и защитных инструментов v1.70.0 | [423-v170-polish.md](423-v170-polish.md) |
|
||||
| [#434](https://github.com/Matysh/houseplan-card/issues/434) Полиш аудита v1.71.0-beta.1 | [434-v171-polish-audit.md](434-v171-polish-audit.md) |
|
||||
| [#443](https://github.com/Matysh/houseplan-card/issues/443) Полиш маршрутов карт робота | [443-vacuum-route-polish.md](443-vacuum-route-polish.md) |
|
||||
| [#474](https://github.com/Matysh/houseplan-card/issues/474) Стартовый граф: арт мебели уходит в ленивый чанк | [474-lazy-furniture-art.md](474-lazy-furniture-art.md) |
|
||||
|
||||
## P3
|
||||
|
||||
|
||||
Reference in New Issue
Block a user