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

31 KiB
Raw Blame History

SPEC-REVIEW-593-r1 — «Обновить встроенную библиотеку мебели до houseplan-0.4.0»

Issue: #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 — предыдущее внедрение той же библиотеки мебели (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 (внедрение библиотеки мебели 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 констатирует «владелец принял варианты по умолчанию по всем трём вопросам 18.09.2026 непосредственно в теле issue» — отдельным действием, с датой, отдельно от вопроса.

Более того, прецедент по этой же самой подсистеме прямо предупреждает именно об этой ошибке: в #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, а не к будущему материалу код-ревью.

Материал раунда

  • Ветка: dev, коммит 073c45b04308 — ребейз его осиротит, и это нормально: ниже якоря, которые ребейз не меняет.
  • Дерево материала: 393fc8925392ed600ae675587da91f34ba6b3372
    git log --all --format='%H %T' | grep 393fc8925392
    
  • Тело issue: 8be001d5a0b2efa4aeb3f2bcd347a9253252dd47c38b091473e6686ca24f91d6
  • Вердикт конвейера: red · High 1