mirror of
https://github.com/Matysh/houseplan-card
synced 2026-09-29 03:09:36 +00:00
Ревью #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
104 lines
31 KiB
Markdown
104 lines
31 KiB
Markdown
# 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
|