# SPEC-REVIEW-474-r1 - **Issue:** #474 — «Стартовый граф: арт мебели уходит в ленивый чанк» - **ТЗ:** `docs/specs/474-lazy-furniture-art.md`, коммит `14afc0e7` (плюс `bb180899` — правка реестра ТЗ, документа не касается) - **SHA материала ревью:** `bb180899ff7932dd81f5a3afc8fe4975ede8df76` - **Трек:** полный. Заход r1, блокирующих циклов израсходовано 0 из 4 до этого вердикта. - **Вердикт: красный** (High-находка ниже блокирует). ## Скоуп Проверялось: тело issue #474 и оба комментария (S2-разбор владельца, публикация ТЗ); `docs/specs/474-lazy-furniture-art.md` целиком; `docs/SCOPE.md`, `AGENTS.md`, `PROCESS.md` §2.3/§2.4/§7.1; текущий код всех модулей, которые ТЗ называет как эталон или точку изменения — `src/i18n/language-runtime.ts` (образец `LanguageRuntime`), `src/editor-runtime-loader.ts` и три его инстанцирования в `houseplan-card.ts` (редактор/онбординг/изометрия), `src/furniture.ts`, `src/furniture-placement.ts`, `src/decor-image-editor.ts`, `src/houseplan-editor-runtime.ts` (все вызовы `furnitureGraphic`/`furniturePathD`), `scripts/bundle-manifest.mjs`, `scripts/bundle-budget.mjs`, `test/core-file-budget.test.mjs`, `demo/golden/harness.mjs`, `demo/smoke_furniture.mjs`, `demo/smoke_space_card_decor_capability.mjs`, `src/i18n/*.json` (образец `toast.locale_load_failed`). Как проверялось: чтением, без сборки и без исполнения смоков — этап spec, продукта ещё нет. Каждое фактическое утверждение ТЗ («единственная точка», «уже так для iso-scene-render», номер потолка, номер строки) сверено с деревом на `bb180899` через `grep`/`Read`, а не принято на слово. ## Находки ### [High] Контракт §4.2–4.3 не даёт `ensure()` для палитры мебели без уже существующей мебели в конфиге — ломает AC7 для главного сценария (первый предмет) **Файл:** `docs/specs/474-lazy-furniture-art.md`, §4.3 «Запуск и бут» и §4.2. **Что заявлено.** Единственная точка запуска `FURNITURE_ART_RUNTIME.ensure()` — приём конфига, и только когда `configNeedsFurnitureArt(config)` находит в пространстве `decor[].kind === 'furniture'` с дизайнерским символом (чистая функция от содержимого конфига). Про открытие палитры/инструмента «Мебель» в редакторе — ни слова. §4.3 «После бута» утверждает: «пользователь добавил первый предмет через редактор → редактор уже импортировал арт статически, чанк в кэше модулей, `ensure()` из приёма конфига разрешается мгновенно» — то есть автор считает статический импорт в `decor-image-editor.ts` достаточным условием того, что палитра увидит арт. **Почему это не так.** Прочитан `src/i18n/language-runtime.ts` — образец, на который ссылается контракт: `state()`/`dictionary()` читают внутренний `Map`, который заполняется **только** внутри `load()`, вызванного из `ensure()`. Простое исполнение модуля (в т.ч. статический импорт откуда-то ещё) этот `Map` не трогает. `FurnitureArtRuntime` по тому же контракту будет вести себя так же: пока никто не вызвал `ensure()` и промис не разрешился, `state()==='pending'` и `art(id)` возвращает `undefined` — независимо от того, сколько мест статически импортировали `furniture-plan-art.generated.ts`. А палитра читает арт именно через это состояние. Сегодня `src/decor-image-editor.ts:283` (`renderFurniturePalette` → `preview(id)`) и `src/houseplan-editor-runtime.ts:4936` (`_renderFurniturePlacementPreview`, живой призрак предмета под курсором) вызывают `furnitureGraphic(id)` — ту же функцию, которую §4.2 переводит на `FURNITURE_ART_RUNTIME.art(id) ?? null`. Ни один из этих двух call sites не вызывает `ensure()` сам и не входит в описанные точки запуска. **Воспроизведение (по коду, не исполнением).** Демо-конфиг (`demo/srv/demo.html`, `CFG.spaces[...]`) не содержит `decor` вовсе — типичный случай «плана без мебели», который §1.2 прямо называет большинством. Смок `demo/smoke_furniture.mjs` начинает проверку палитры с явного `c._curSpaceCfg.decor = []` (нет ни одного предмета мебели ни в одном пространстве конфига) и затем требует `prevD = item('sofa')?.querySelector('svg.furnprev path')?.getAttribute('d')`, `out.previewIsDrawn = prevD.length > 10` — то есть палитра обязана показывать настоящий арт сразу при открытии, до какого-либо сохранения. По контракту ТЗ в этой точке `configNeedsFurnitureArt(config)` вернёт `false` (в конфиге нет `decor[].kind==='furniture'`), `ensure()` никогда не вызывался, `state()` — `'pending'`, `furnitureGraphic('sofa')` вернёт `null` → `prevD === ''` → `previewIsDrawn === false`. AC7 («Редактор: палитра и размещение как прежде», доказательство — `smoke_furniture`) провалится не по вине реализации, а потому, что контракт ТЗ не предусматривает загрузку арта для этого пути вообще. Тот же разрыв — для живого призрака мыши под курсором (`_renderFurniturePlacementPreview`) при размещении самого первого предмета: он тоже останется `nothing` до первого сохранения, то есть предмет непрозрачно не виден, пока пользователь его не поставит вслепую. **Чем это грозит вне смока.** Персона из сценария (§1.1) добавляет первую мебель через редактор на плане, где мебели ещё не было, — ровно кейс, который ломается. Это прямое противоречие §1.2 («После: визуально ничего не меняется») применительно к редактору, а не только к View. **Что нужно ТЗ.** Явно назвать третью точку запуска `ensure()` — независимую от содержимого конфига, привязанную к входу в инструмент/палитру «Мебель» (например, конструктор `DecorImageEditor` или момент активации `_decorTool === 'furniture'`). Как именно — решение автора (место гварда, это техническая деталь), но контракт обязан гарантировать, что палитра не открывается раньше, чем `ensure()` хотя бы запущен, и описать, что показывается, пока он не разрешился (те же 44 пустых превью, что и без правки, или ожидание) — иначе AC7 не проверяем как «без изменений». ## Что проверено и корректно - Сценарий и «что человек увидит» (§1.1–1.2) отвечают на оба продуктовых вопроса §7.1, ссылаются на персону из `docs/SCOPE.md`. - Разрез каталог/арт (§4.1): единственная точка чтения арта во View — `houseplan-card.ts:8917` (`const art = furnitureGraphic(sh.symbol);`, у ТЗ «8915» — расхождение на 2 строки, не содержательно и не блокирует), магнит (`furniture-placement.ts:170`) использует только размеры — подтверждено чтением, разрез проходит по границе, которую ТЗ называет. - Образец `LanguageRuntime` (page-scoped синглтон, `ready|pending|fallback`, нонс на повторной попытке, осевший `fallback`) реально устроен так, как описано в §4.2, и реально является module-level singleton (`LANGUAGE_RUNTIME` в `src/i18n/registry.ts`, разделяемый `houseplan-card`, `editor`, `space-card`, `space-editor`) — заявление о «нескольких карточках на странице» (AC11) технически обосновано этим прецедентом. - Нонс-механизм (`hp_retry`, замена токена `__HOUSEPLAN_*_RETRY_ASSET__` в `scripts/bundle-manifest.mjs` после сборки) — реальный существующий паттерн для трёх лениво грузимых модулей (редактор/онбординг/изометрия) и словарей de/fr; перенос на арт мебели — техническая деталь без нужды спрашивать владельца. - `core-file-budget`: `src/houseplan-card.ts` сейчас **13 658** строк при потолке **13 659** — подтверждает заявление ТЗ «ядро стоит ровно на потолке» и то, что требование «+0 строк» не декоративное, а обязательное условие AC10. - `houseplan-space-card` действительно не рисует `decor`/`furniture` вообще (`grep` по `src/space-card.ts` не находит ни kind-ветвления, ни импорта `furniture*`) — формулировка не-скоупа (§3) точна, условная фраза «если рисует» корректно не выдаёт домысел за факт. - i18n: ключ `toast.furniture_art_load_failed` не занят, структура совпадает с образцом `toast.locale_load_failed` в en/de/fr/ru (`src/i18n/*.json:677`). - AC1…AC11 пронумерованы, у каждого указан способ доказательства (unit / смок / golden / `bundle:budget`); §9 явно помечает численный порог AC1 как предположение с запасом, а не факт — соответствует требованию «размытое место не додумывается». - Обязательные разделы §7.1 все присутствуют: сценарий, что человек увидит, проблема, скоуп/не-скоуп, контракт, UX/модель данных/i18n (6.1), AC (6), риски (6.2), откат (5), release-артефакты (7), затронутые файлы (8), явный блок предположений (9). - Связь issue ↔ ТЗ в обе стороны на месте (`docs/specs/README.md:189`). ## Замечание вне блокирующего (Low, оставлено на усмотрение автора) - §6.2, строка риска «Golden-сцены с мебелью станут недетерминированными», мера — «harness ждёт `FURNITURE_ART_RUNTIME.state()==='ready'` перед кадром, **как ждёт язык**». В `demo/golden/harness.mjs` нет ни одного ожидания `LANGUAGE_RUNTIME` — такого прецедента не существует. Реальный работающий прецедент в этом файле — `ensureIsoRuntime(card)` (строки ~816–823): явный вызов card-экспонированного метода `card._ensureIsoSceneRuntime()` для сцен, которым нужна изометрия. Аналогия неточна, но вывод («harness должен явно дождаться») верен и достижим тем же способом — просто не «как язык», а «как изометрия». Не блокирует, т.к. не меняет ни один AC и подхватится golden-гейтом в code review, если не будет сделано. ## Чего не проверял - Численные пороги AC1 (`≤ 290 000`, потолок `≈ 289 500`) — ТЗ само помечает их предположением до фактического замера на итоговом дереве; пересчёт — задача code review (`npm run bundle:budget`). - Реализуемость шести свидетелей §4.6 как мутантов в `scripts/mutation-gate.mjs` — на этапе spec кода ещё нет, откладывается на code review. - Гейты (`typecheck`/`test`/`build`/`bundle:budget`/`golden`/смоки) не гонялись: этап spec, продуктового кода по задаче ещё нет — прогон был бы гейтом на пустом месте. - `docs/ARCHITECTURE.md` / `docs/FURNITURE.md` содержимое правок (§7) — раздел верно называет, какие абзацы менять, сам текст правок появится в реализации. --- ## Материал раунда - Ветка: `issue/474-lazy-furniture-art`, коммит `bb180899ff79` — ребейз его осиротит, и это нормально: ниже якоря, которые ребейз не меняет. - Дерево материала: `a39b37362d8e58df7e13f58dfecb9aed1571f56d` ``` git log --all --format='%H %T' | grep a39b37362d8e ``` - ТЗ `docs/specs/474-lazy-furniture-art.md`, блоб `9ee5c6fe59213617ad8b6374ce81c19460285cac` ``` git log --all --find-object=9ee5c6fe59213617ad8b6374ce81c19460285cac -- docs/specs/474-lazy-furniture-art.md ```