15 KiB
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— ребейз его осиротит, и это нормально: ниже якоря, которые ребейз не меняет. - Дерево материала:
a39b37362d8e58df7e13f58dfecb9aed1571f56dgit log --all --format='%H %T' | grep a39b37362d8e - ТЗ
docs/specs/474-lazy-furniture-art.md, блоб9ee5c6fe59213617ad8b6374ce81c19460285cacgit log --all --find-object=9ee5c6fe59213617ad8b6374ce81c19460285cac -- docs/specs/474-lazy-furniture-art.md