mirror of
https://github.com/Matysh/houseplan-card
synced 2026-10-03 13:18:58 +00:00
176 lines
15 KiB
Markdown
176 lines
15 KiB
Markdown
# 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
|
||
```
|