Files
houseplan-card/legacy/reviews/v1.77.0/SPEC-REVIEW-593-r1.md
T
Claudeandclaude[bot] 0991c45374 fix(tools): архив переписывает относительные ссылки перенесённых документов (#682)
Ревью #682 r1, Medium: перенос добавляет документу уровень вложенности
(`docs/reviews/X.md` → `legacy/reviews/<тег>/X.md`, `docs/specs/` →
`legacy/specs/`), а относительные ссылки внутри перенесённых документов и в
соседях, ссылавшихся на них, никто не пересчитывал — на `97d19268` 53 битые
ссылки в 46 файлах (заявление «все 26 резолвятся» в `7feb6177` было верно
только до переноса документов ревью). Гейты архив не смотрят.

`reviews-archive.mjs`: `repairLinks` пересчитывает ссылку, если она не
резолвится от нового места, а цель находится от нового или старого места
через карту переносов; битая и до переноса ссылка не трогается. `--apply`
делает это само, `--repair-links=<rev>` — для всех переименований
`<rev>..HEAD`, `--check-links` печатает битые. Этим коммитом
`--repair-links=origin/dev` переписал ровно 53 ссылки в 46 файлах; остались
две прежние «...»-заглушки в CODE-REVIEW-448-r2 (битые и на dev). Тесты:
перенесённый документ, сосед со ссылкой в архив, ТЗ со ссылкой на позже
перенесённое ревью, битая-до-переноса не трогается, в `legacy/` битых нет;
мутант `reviews-archive-links-from-new-place-only`. PROCESS §2.10 и
DEVELOPMENT › Release называют переписывание и `--check-links`.

Issue: #682
User-Visible: no
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018qZfe7YS4rqEMKoVeS3GKd
2026-09-27 22:10:47 +00:00

104 lines
31 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-593-r1 — «Обновить встроенную библиотеку мебели до houseplan-0.4.0»
Issue: [#593](https://github.com/Matysh/houseplan-card/issues/593)
Этап: spec (полный трек — задача сама называет критерий §5, который не проходит: «одна поверхность» нарушена явно, затронуты `src/**`, генератор, i18n, PDF-путь, golden и бюджеты)
Заход: r1 (первый; разделы «Унаследовано из r0» и «Закрытие раунда r0» не нужны — §2.10 применяется со второго захода)
## Вердикт
**Красный.** High: 1. Medium в скоупе: 1 (возвращается автору в этой же задаче). Medium вне скоупа: 0. Low: 0.
## Скоуп разбора
Полный разбор: тело issue #593 (раздел `## ТЗ` целиком, плюс сохранённое предложение автора над ним — ТЗ явно ссылается на него как на действующий текст там, где не оспорено), единственный комментарий issue (аналитика S2, `#issuecomment-5737664216`), полная временная шкала событий issue (`gh api .../timeline`), `docs/SCOPE.md`, `AGENTS.md`, `PROCESS.md` (§2.2, §2.4, §7.1, §7.2, §4, §5), прецедентная задача [#159](https://github.com/Matysh/houseplan-card/issues/159) — предыдущее внедрение той же библиотеки мебели (0.3.0), поскольку её спор-ревью содержит находку по тому же классу дефекта, что найден здесь. Поскольку почти все содержательные утверждения ТЗ о текущем состоянии репозитория — проверяемые факты, а не предположения о будущем, разбор включает и сам код:
- `src/furniture.ts` — фактическая структура `FURNITURE`/`BY_ID`, состав `RETAINED_IDS` (12 id), `default` размеры всех 12 retained-символов, использование `symbol.designer` в `furnitureGraphic`/`furniturePathD`/`furnitureArtIsLazy`;
- `src/decor-image-editor.ts:275-277` — фактический механизм скрытия пустых категорий палитры (`GENERATED_FURNITURE_MENU.filter(item => allSymbols.some(symbol => symbol.category === item.id))`), на который ссылается Р1;
- `src/i18n/{ru,en,fr,de}.json` — точные количества ключей `furn.cat_*` (29) и `furn.sym_*` (56) во всех четырёх локалях;
- `docs/FURNITURE.md` (разделы «Library and source», «Compatibility and performance») — точность цитаты «the 12 retained primitive symbols draw regardless»;
- `scripts/bundle-manifest.mjs`, `scripts/bundle-budget.mjs` — механизм расчёта `lazyFurnitureArtGzipBytes`/`lazyEditorGzipBytes` и то, какие из них реально сравниваются с потолком, а какие только печатаются;
- `scripts/mutation-registry.mjs` — отсутствие коллизий с тремя предложенными id мутантов;
- `demo/golden/matrix.mjs` — существование всех 10 названных сцен (включая `isometric-stage3-overlays-dark`, сгенерированную циклом по `['light','dark']`, и точный состав фикстуры `furniture-plan-art-dark`, где `fridge` действительно используется как «retained»-предмет);
- `demo/smoke_furniture.mjs`, `demo/smoke_furniture_lazy_art.mjs`, `demo/smoke_furniture_polish.mjs` — существование файлов;
- `package.json` — существование всех названных npm-скриптов (`furniture:generate`, `furniture:check`, `bundle:budget`, `gate:small`, `docs:accept`).
## Как проверялось
1. **Временная шкала issue.** `gh api repos/Matysh/houseplan-card/issues/593/timeline` — задача заведена сторонним контрибьютором `nikitaevfz-commits` 2026-09-18T18:20:39Z, владелец/агент под учёткой `Matysh` пометил `S1-new` в 19:33:52. Единственный комментарий issue (аналитика S2 + предложение продуктовых решений) опубликован в 00:05:53. **Метка `S1-new` снята и `S3-spec` поставлена в 00:05:57 — через 4 секунды после публикации комментария**, то есть без какой-либо паузы между постановкой двух продуктовых вопросов и записью в ТЗ «Принято». `S4-spec-review` поставлена в 00:12:35 — ещё через 7 минут. Метка `blocked` не применялась ни разу за всю историю issue.
2. Сверил текст комментария S2 построчно с §3 ТЗ: оба продуктовых вопроса (кактус/категория, поведение при отказе ленивого чанка) заданы там с предлагаемым дефолтом и немедленно закрыты фразой «Молчание считаю согласием с обоими вариантами по умолчанию» — в том же сообщении, тем же автором, без второго, независимого голоса.
3. Перечитал §2.2 и §7.1 `PROCESS.md`: правило «молчание владельца — согласие» текстуально ограничено аналитическими оценками (ценность/сложность/трек, п. 2.2) и явно **не** распространяется на продуктовые вопросы, которые «Единственный класс вопросов, который вообще задаётся владельцу» и для которых предписан отдельный протокол — batched-вопрос с дефолтом, `blocked` поверх `S3-spec`, и **ожидание ответа**. Ни `blocked`, ни ответ не появились.
4. Нашёл прямой прецедент в этом же проекте на этой же подсистеме: [#159](https://github.com/Matysh/houseplan-card/issues/159) (внедрение библиотеки мебели 0.3.0) — r1 спек-ревью поставило **High-1** ровно за то, что подтверждение авторства/лицензии на самом деле не было опубликовано владельцем, хотя аналитика формулировала это как решённый факт; спецификация была заблокирована до тех пор, пока `Matysh` не опубликовал явный комментарий от первого лица («Я являюсь автором всех 77 SVG… и разрешаю… на условиях MIT License…»). §10/AC10 текущего ТЗ корректно учит этот урок для лицензии (явно требует нового публичного комментария владельца, ссылается на #159 сама), но **тот же урок не применён к Р1/Р2** — они получили штамп «Принято» без аналога такого комментария.
5. Проверил механизм скрытия пустых категорий (Р1): `src/decor-image-editor.ts:275-277` действительно фильтрует `GENERATED_FURNITURE_MENU` по наличию хотя бы одного символа этой категории — специального кода на скрытие `exercise` не нужно, заявление ТЗ точное.
6. Проверил `docs/FURNITURE.md:144-146` — цитата «the 12 retained primitive symbols draw regardless» подтверждена дословно; это тот самый пользовательский контракт, который Р2 отменяет.
7. Проверил все числовые и структурные факты о текущем коде на предмет точности (не догадки): `RETAINED_IDS` — ровно `fridge, dishwasher, washer, dryer, ac, water_heater, shower, sink, stairs, fireplace, plant, rug` (12, совпадает с ТЗ); default-размеры всех 12 сверены построчно с `src/furniture.ts` (fridge 60×65, dishwasher/washer/dryer 60×60, ac 90×25, water_heater 45×45, shower 90×90, sink 60×45, stairs 100×280, fireplace 120×40, plant 40×40, rug 200×140) — совпадают; `furn.cat_*`=29 и `furn.sym_*`=56 в каждой из 4 локалей (`ru/en/fr/de`) — совпадает; ловушка объединения `FURNITURE = [...GENERATED_FURNITURE_CATALOG.map(...designer:true), ...LEGACY_FURNITURE.filter(RETAINED_IDS.has)]` и `BY_ID = new Map(...)` — совпадает построчно с `src/furniture.ts:251-263`; список файлов-потребителей `symbol.designer`/`furnitureArtIsLazy` (`src/furniture-placement.ts:170`, `src/houseplan-card.ts` вызовы `ensureFurnitureArtFor`/`furnitureArtBootPending`, `demo/golden/harness.mjs:1003`) — реальны, не выдуманы, хотя и не содержат буквальных строк `LEGACY_FURNITURE`/`RETAINED_IDS` (проверено отдельным grep, чтобы не спутать «упоминает удаляемый идентификатор» с «зависит от удаляемого поведения»).
8. Проверил доказуемость AC9 (защитный AC, три бюджета §7): прочитал `scripts/bundle-manifest.mjs` и `scripts/bundle-budget.mjs` целиком в интересующей части. `buildBundleManifest` действительно считает `lazyFurnitureArtGzipBytes` — но эта величина складывается **только** из графа, чьи модули достижимы из чанка, содержащего `furniture-plan-art.generated.ts` (роль `'furniture-art'`, `scripts/bundle-manifest.mjs:95-98`). У `furniture-menu-art.generated.ts` (файл, который ТЗ называет источником бюджета «lazy editor/menu-art», +6 KiB) **нет собственной роли** в классификаторе чанков — он импортируется из `decor-image-editor.ts`, достижимого только из `houseplan-editor-runtime.ts`, и поэтому целиком тонет в общей роли `'editor'`/`lazyEditorGzipBytes` вместе со всем остальным кодом обоих редакторов. Ни `lazyFurnitureArtGzipBytes`, ни `lazyEditorGzipBytes` не сравниваются ни с каким порогом нигде в `assertBundleBudget` (`scripts/bundle-budget.mjs:399-450`) — обе величины только печатаются в отчёте (`:456,459,480`). Единственная реальная числовая граница во всём `bundle:budget` — абсолютный потолок `INITIAL_VIEW_GZIP_CEILING`/`_BUDGET` для initial View (это тоже не то же самое, что «дельта +1 KiB для этой задачи», которую заявляет §7 — потолок абсолютный, а не относительно текущего значения). Подробности и последствие — находка М1 ниже.
9. Все 10 golden-сцен, названных в §8, реально существуют в `demo/golden/matrix.mjs`: девять — буквальными id, `isometric-stage3-overlays-dark` — генерируется циклом `['light','dark'].map(theme => ({id: \`isometric-stage3-overlays-${theme}\`, ...}))` (первый grep по буквальной строке этого не нашёл — отдельно перепроверил вручную, чтобы не завести ложную находку). Состав `furniture-plan-art-dark` подтверждён: `furn-retained` действительно использует `symbol: 'fridge'`.
10. Названные npm-скрипты (`furniture:generate`, `furniture:check`, `bundle:budget`, `gate:small`, `docs:accept`) существуют в `package.json`; флаги `--expect-change`/`--expect-new` golden-приёмки существуют в `scripts/golden-accept.mjs`/`scripts/golden-acceptance.mjs`.
11. Классы риска §2.6 (в применении к спецификации, а не коду) — обе продуктовые ветки (данные/права: «несколько источников» — ровно ситуация Р1/Р2 ниже; host/input, визуал, геометрия) разобраны в ТЗ явно; отдельного дефекта по остальным классам не нашёл.
## Обязательные разделы §7.1 — комплектность
Присутствуют все: сценарий (§1 «Кто, где, когда») · что человек увидит до/после (§1) · проблема (унаследована из сохранённого предложения автора) · скоуп/не-скоуп (§4 ТЗ + «Не входит» сохранённого предложения, не оспорено) · контракт поведения (§3, §4) · UX (унаследовано из предложения + §4 п.4) · модель данных и миграция (§4 п.1-2, «Каталог и совместимость» предложения) · i18n (§6) · AC1…AC11 с доказательством (§10, таблица «AC · чем доказан · чем краснеет») · план автотестов (§8) · риски (§11) · откат (§4 п.1, §12 п.3) · release-артефакты (§9). Комплектность разделов дефектов не имеет.
## Находки
### High (блокирует)
**H1. Оба продуктовых решения (Р1 «кактус → `plant`», Р2 «при отказе чанка не рисуется ничего») помечены в ТЗ словом «Принято», но фактически не подтверждены владельцем — только самим автором ТЗ в том же сообщении, где вопрос и был задан, без паузы и без независимого ответа.**
Воспроизведение: `gh api repos/Matysh/houseplan-card/issues/593/timeline` показывает единственный комментарий issue (00:05:53) с текстом «Два продуктовых вопроса владельцу» и тут же — «Молчание считаю согласием с обоими вариантами по умолчанию: ТЗ пишу на них». Метка `S3-spec` появляется через 4 секунды после этого комментария (00:05:57), `S4-spec-review` — ещё через 7 минут (00:12:35). Метка `blocked` не применялась вовсе. Второго комментария, письма или иного независимого источника, где владелец лично подтверждает выбор, в issue нет.
`PROCESS.md` §7.1 резервирует «молчание — согласие» **только** за аналитическими оценками этапа S2 (§2.2: «Комментарий аналитики — уведомление, а не запрос»); для продуктовых вопросов этапа ТЗ предписан другой протокол — вопрос пачкой с дефолтом, `blocked` поверх `S3-spec`, и ожидание фактического ответа. Прецедент того, как это должно выглядеть, в этом же проекте: [SPEC-REVIEW-588-r1](SPEC-REVIEW-588-r1.md) констатирует «владелец принял варианты по умолчанию по всем трём вопросам 18.09.2026 непосредственно в теле issue» — отдельным действием, с датой, отдельно от вопроса.
Более того, прецедент по этой же самой подсистеме прямо предупреждает именно об этой ошибке: в [#159](https://github.com/Matysh/houseplan-card/issues/159) (внедрение библиотеки мебели 0.3.0) reviewer поставил **High-1** за то, что подтверждение авторства/MIT было дано «только в рабочей сессии» и не публично, хотя аналитика формулировала это как решённый факт — задача была заблокирована до тех пор, пока `Matysh` не опубликовал отдельный явный комментарий от первого лица. Автор текущего ТЗ этот урок применил к AC10 (лицензия) буквально, сославшись на #159 по номеру, но не применил его к собственным Р1/Р2 — те получили формулировку «Принято» без аналогичного независимого голоса.
Почему это не техническая деталь, которую я как ревьюер вправе решить сам: оба вопроса — ровно то, что §7.1 называет продуктовым («что человек видит или делает»). Р2 к тому же не нейтральна: она отменяет действующее обещание `docs/FURNITURE.md` («12 retained primitive symbols draw regardless») и заменяет его на «ничего не рисуется до перезагрузки» для всех 60 предметов при отказе сети — реальная деградация поведения при плохом канале, которую нельзя тихо решить дефолтом агента. Р1 меняет видимую категоризацию палитры (32 непустые категории вместо заявленных в исходном предложении 33). Обе принадлежат исключительно владельцу; вердикт ревью не может их заменить (`PROCESS.md` §7.1: «Технический спор автора и ревьюера решается вердиктом… продуктовое — нет»).
Чем закрывается: issue возвращается в `S3-spec` с `blocked`; в issue нужен отдельный явный комментарий владельца (не переиспользующий аналитический), подтверждающий или меняющий оба дефолта — по образцу того, что уже сделано для AC10.
### Medium (в скоупе задачи — чинится в этой же задаче)
**М1. AC9 и §7 заявляют защиту трёх бюджетов через `npm run bundle:budget`, но код гейта фактически ограничивает потолком только один из трёх, а два остальных лишь печатает.** `scripts/bundle-manifest.mjs` считает `lazyFurnitureArtGzipBytes` исключительно по графу `furniture-plan-art.generated.ts` (роль `'furniture-art'`, `:95-98`); у `furniture-menu-art.generated.ts` собственной роли нет — он тонет в общей `lazyEditorGzipBytes` вместе со всем остальным кодом обоих редакторов (десятки других модулей). `scripts/bundle-budget.mjs:441-450` (`assertBundleBudget`) сравнивает с порогом только `initialViewGzipBytes` и `initialPanelOnlyGzipBytes`; `lazyFurnitureArtGzipBytes`/`lazyEditorGzipBytes` встречаются в файле только в объекте результата и в печати отчёта (`:456,459,480`) — ни разу в условии `throw`. Формулировка `AC9` «чем краснеет: превышение полосы потолка» верна только для initial View (у него есть механизм `INITIAL_VIEW_CEILING_BAND`/`LOW_HEADROOM_ACKNOWLEDGED_CEILING`); для двух других предполагаемых пределов (+8 KiB lazy plan-art, +8 KiB lazy editor/menu-art) такого механизма не существует нигде в дереве. Даже «+1 KiB» для initial View — это дельта конкретной задачи, а не абсолютный потолок; `assertBundleBudget` не знает о задаче и не проверяет дельту, только абсолютное число против общего `INITIAL_VIEW_GZIP_BUDGET`/`_CEILING`, у которого сейчас большой запас.
Следствие: если реализация превысит +8 KiB на любом из двух ленивых графов, ни один автоматический гейт этого не заметит — красным этот AC не станет никогда, только собственное внимательное чтение отчёта на код-ревью (если кто-то вспомнит на что смотреть).
Чем закрывается в скоупе этой задачи — один из двух путей, выбор оставляю автору как техническое решение: (а) явно понизить формулировку AC9 до «числа сверяются чтением отчёта `bundle:budget` (`lazyFurnitureArtGzipBytes`, `lazyEditorGzipBytes`), автоматической защиты для двух ленивых бюджетов нет» — так третий столбец таблицы AC перестаёт вводить в заблуждение; либо (б) добавить в `scripts/bundle-budget.mjs` два новых порога и `throw` при превышении — тогда AC9 действительно доказывается тем, что заявлено. Пустая/неверная защита в третьем столбце — находка по духу требования §2.7 «таблица чем краснеет», применённого здесь на этапе спецификации: сейчас столбец не пуст, но называет механизм, который эти две величины не проверяет.
## Что проверено и корректно
- Комплектность §7.1 — все обязательные разделы на месте (см. раздел выше).
- Все проверяемые фактические утверждения ТЗ о текущем состоянии кода, которые удалось сверить чтением, подтвердились без расхождений: состав и default-размеры 12 retained-символов, число ключей i18n (29/56 × 4 локали), формулировка `docs/FURNITURE.md`, механизм скрытия пустых категорий палитры, ловушка двойного объединения `FURNITURE`, список реальных потребителей удаляемого кода, состав golden-сцен и npm-скриптов. Ни одной находки в духе «утверждение о поведении выдано за факт без основания» — все технические заявления либо доказаны запуском реального генератора на присланном пакете (аналитика S2), либо чтением действующего кода.
- Р1/Р2 как **содержание** предложенных решений разумны и не противоречат `docs/SCOPE.md` (обе — в рамках job J4/J6, не расширяют скоуп); претензия H1 — исключительно к тому, что они не подтверждены владельцем, а не к тому, что дефолты плохи.
- AC10 корректно учитывает урок #159: требует нового явного публичного комментария владельца для лицензии, не выдаёт уже случившееся обсуждение за подтверждение.
- Раздел «Принято предположительно» (§12) корректно разделяет продуктовые пункты 1-2 (которые меняются только словом владельца) и технические 3-7 (которые вправе оспорить ревьюер) — само разделение правильное; дефект H1 в том, что пункты 1-2 в тексте уже выданы за решённые, хотя по собственной классификации ТЗ ещё не решены.
- Бюджетные числа (gzip до/после по трём generated-файлам) в §2 аналитики и §7 ТЗ проверены переносом из фактического прогона генератора, а не придуманы — совпадают между собой во всех местах, где повторяются.
- Мутанты `furniture-legacy-shadows-designer-art`, `furniture-pack-accepts-foreign-symbol-count`, `furniture-cactus-lands-in-exercise` не конфликтуют по имени с `scripts/mutation-registry.mjs`.
## Чего не проверял
- Содержимое приложенного `houseplan-furniture-0.4.0.zip` (пакет, `pack.json`, все 93 SVG) — файл не в репозитории и не мой предмет на этапе ревью ТЗ; полагаюсь на утверждение аналитики S2, что реальный присланный пакет уже прогнан через `scripts/generate-furniture-assets.mjs` с ослаблением ровно трёх констант и дал `OK: 60 plan symbols, 33 menu icons` — цифры из этого прогона (gzip-дельты, соответствие group/category) внутренне непротиворечивы и совпадают между аналитикой и ТЗ, но байт в байт zip не разбирал. Это предмет код-ревью — там пакет уже будет в дереве, и `furniture:check`/`test/furniture-assets.test.mjs` дадут исполнимый oracle.
- Не проверял дубликаты (аналогичные issue) — комментарий аналитики не содержит явного пункта «дубликаты», в теле ТЗ тоже не поднимается; не нашёл признаков дублирования по названию/содержанию, но целенаправленно не искал.
- Не запускал `npm run typecheck`, `npm test`, `npm run build`, `npm run bundle:budget`, `golden:verify` — на этапе ревью ТЗ продуктовый код ещё не менялся, эти гейты не относятся к предмету этого раунда. Прогонял только `node scripts/generate-furniture-assets.mjs`-эквивалентные проверки не потребовалось: пакет 0.4.0 не в дереве, генератор не запускался мной — использовал существующий вывод аналитики S2 и собственное чтение `scripts/generate-furniture-assets.mjs`/`bundle-manifest.mjs`/`bundle-budget.mjs`.
- Не проверял `custom_components/houseplan/**/*.py` — задача явно не задевает backend (persisted config schema не меняется, подтверждено и в теле issue, и структурно: мебель хранится в decor-слое конфига, серверной валидации id мебели нет).
- Не сверял русские/английские/французские/немецкие переводы новых 28 строк i18n на смысл — они появятся только при реализации; на этапе спецификации проверил только точность утверждения о количестве существующих ключей.
## Материал раунда
- Issue #593, тело на момент разбора (`gh issue view 593 --json body`) и единственный комментарий (`gh issue view 593 --json comments`); SHA-256 нормализованного тела вписывается конвейером публикации в блок якорей, здесь не дублируется.
- Заход r1, циклов ревью ТЗ израсходовано 0 из 4 до этого раунда (полный трек, лимит циклов — 4 по §4); текущий красный вердикт израсходует цикл 1/4 после публикации.
- Код сверялся на рабочей копии репозитория в состоянии `HEAD` (`073c45b04308cd613182c700f30c75ebfa8d2ca7`) на момент разбора — продуктовый код под эту задачу ещё не менялся, все ссылки на строки относятся к текущему `dev`, а не к будущему материалу код-ревью.
---
<!-- material-anchors: сгенерировано конвейером (#414) -->
## Материал раунда
- Ветка: `dev`, коммит `073c45b04308` — ребейз его осиротит, и это нормально: ниже якоря, которые ребейз не меняет.
- Дерево материала: `393fc8925392ed600ae675587da91f34ba6b3372`
```
git log --all --format='%H %T' | grep 393fc8925392
```
- Тело issue: `8be001d5a0b2efa4aeb3f2bcd347a9253252dd47c38b091473e6686ca24f91d6`
- Вердикт конвейера: `red` · High 1