Files
houseplan-card/docs/reviews/SPEC-REVIEW-474-r1.md
T
2026-09-06 16:10:01 +03:00

15 KiB
Raw Blame History

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