From c0426dadd815323ac58230641fae1a3ba81b4d05 Mon Sep 17 00:00:00 2001 From: "claude[bot]" <209825114+claude[bot]@users.noreply.github.com> Date: Sun, 6 Sep 2026 12:16:26 +0000 Subject: [PATCH] docs: review document for #474 Issue: #474 User-Visible: no --- docs/reviews/SPEC-REVIEW-474-r1.md | 175 +++++++++++++++++++++++++++++ 1 file changed, 175 insertions(+) create mode 100644 docs/reviews/SPEC-REVIEW-474-r1.md diff --git a/docs/reviews/SPEC-REVIEW-474-r1.md b/docs/reviews/SPEC-REVIEW-474-r1.md new file mode 100644 index 00000000..d7f8e34a --- /dev/null +++ b/docs/reviews/SPEC-REVIEW-474-r1.md @@ -0,0 +1,175 @@ +# 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 + ```