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

176 lines
15 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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) — раздел
верно называет, какие абзацы менять, сам текст правок появится в реализации.
---
<!-- material-anchors: сгенерировано конвейером (#414) -->
## Материал раунда
- Ветка: `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
```