Ревью #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
31 KiB
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).
Как проверялось
- Временная шкала issue.
gh api repos/Matysh/houseplan-card/issues/593/timeline— задача заведена сторонним контрибьюторомnikitaevfz-commits2026-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. - Сверил текст комментария S2 построчно с §3 ТЗ: оба продуктовых вопроса (кактус/категория, поведение при отказе ленивого чанка) заданы там с предлагаемым дефолтом и немедленно закрыты фразой «Молчание считаю согласием с обоими вариантами по умолчанию» — в том же сообщении, тем же автором, без второго, независимого голоса.
- Перечитал §2.2 и §7.1
PROCESS.md: правило «молчание владельца — согласие» текстуально ограничено аналитическими оценками (ценность/сложность/трек, п. 2.2) и явно не распространяется на продуктовые вопросы, которые «Единственный класс вопросов, который вообще задаётся владельцу» и для которых предписан отдельный протокол — batched-вопрос с дефолтом,blockedповерхS3-spec, и ожидание ответа. Ниblocked, ни ответ не появились. - Нашёл прямой прецедент в этом же проекте на этой же подсистеме: #159 (внедрение библиотеки мебели 0.3.0) — r1 спек-ревью поставило High-1 ровно за то, что подтверждение авторства/лицензии на самом деле не было опубликовано владельцем, хотя аналитика формулировала это как решённый факт; спецификация была заблокирована до тех пор, пока
Matyshне опубликовал явный комментарий от первого лица («Я являюсь автором всех 77 SVG… и разрешаю… на условиях MIT License…»). §10/AC10 текущего ТЗ корректно учит этот урок для лицензии (явно требует нового публичного комментария владельца, ссылается на #159 сама), но тот же урок не применён к Р1/Р2 — они получили штамп «Принято» без аналога такого комментария. - Проверил механизм скрытия пустых категорий (Р1):
src/decor-image-editor.ts:275-277действительно фильтруетGENERATED_FURNITURE_MENUпо наличию хотя бы одного символа этой категории — специального кода на скрытиеexerciseне нужно, заявление ТЗ точное. - Проверил
docs/FURNITURE.md:144-146— цитата «the 12 retained primitive symbols draw regardless» подтверждена дословно; это тот самый пользовательский контракт, который Р2 отменяет. - Проверил все числовые и структурные факты о текущем коде на предмет точности (не догадки):
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, чтобы не спутать «упоминает удаляемый идентификатор» с «зависит от удаляемого поведения»). - Проверил доказуемость 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 ниже. - Все 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'`. - Названные npm-скрипты (
furniture:generate,furniture:check,bundle:budget,gate:small,docs:accept) существуют вpackage.json; флаги--expect-change/--expect-newgolden-приёмки существуют вscripts/golden-accept.mjs/scripts/golden-acceptance.mjs. - Классы риска §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— ребейз его осиротит, и это нормально: ниже якоря, которые ребейз не меняет. - Дерево материала:
393fc8925392ed600ae675587da91f34ba6b3372git log --all --format='%H %T' | grep 393fc8925392 - Тело issue:
8be001d5a0b2efa4aeb3f2bcd347a9253252dd47c38b091473e6686ca24f91d6 - Вердикт конвейера:
red· High 1