fix(cover): restore sub-icon slot in cover.svg (#201)

* fix(cover): restore sub-icon slot in cover.svg

* docs(cover): add ADR-089 and handoff for PR #201

---------

Co-authored-by: opencode-agent <agent@opencode.local>
This commit is contained in:
Sergey 2026-08-01 02:56:40 +03:00 committed by GitHub
parent 87d6c4464b
commit 85d7d62fcd
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 55 additions and 1 deletions

View file

@ -1,5 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" width="1024" height="1024" viewBox="0 0 1024 1024"> <svg xmlns="http://www.w3.org/2000/svg" width="1024" height="1024" viewBox="0 0 1024 1024">
<!-- slot: name=icon, x=252, y=180, w=520, h=520, fit=contain, recolor=accent --> <!-- slot: name=icon, x=252, y=180, w=520, h=520, fit=contain, recolor=accent -->
<!-- slot: name=sub-icon, x=412, y=600, w=200, h=200, fit=contain, recolor=accent -->
<rect width="1024" height="1024" fill="{{base}}" /> <rect width="1024" height="1024" fill="{{base}}" />
<text x="512" y="830" font-family="Geist Sans, sans-serif" font-size="80" font-weight="700" fill="{{fg}}" text-anchor="middle">{{title}}</text> <text x="512" y="830" font-family="Geist Sans, sans-serif" font-size="80" font-weight="700" fill="{{fg}}" text-anchor="middle">{{title}}</text>
</svg> </svg>

Before

Width:  |  Height:  |  Size: 382 B

After

Width:  |  Height:  |  Size: 470 B

View file

@ -0,0 +1,31 @@
# ADR-089: Restore sub-icon slot in production cover.svg (PR#201)
## Статус
Accepted (2026-07-31)
## Контекст
PR#197 (ADR-087) упростил продакшн-шаблон `templates/cover.svg` до единственного `icon`-слота 520×520 + `{{title}}` y=830, удалив slot-комментарии `sub-icon`, `badge` и subtitle-`<text>`. Это было демонстрацией отвязки тестов от продакшн-шаблона (тесты переехали на `tests/fixtures/cover.svg`). Удалённые слоты в продакшн-рендере тогда не использовались, и `assets/cover.png` остался побайтово идентичным.
Однако движок `buildSvg` итерирует по слотам, **объявлённым в шаблоне** (`slot-parser` читает HTML-комментарии из шаблона), а не по `args.slots`, переданным в вызов. Поэтому после PR#197 переданный `sub-icon=...` молча отбрасывался — слот не объявлен в шаблоне → рендерер его не видит.
Команда `/cover` (PR#199, ADR-088) документирует `sub-icon` как optional-слот и передаёт его в `draw-image` (пример: `sub-icon=opencode`). Возникла рассогласованность: команда обещает `sub-icon`, рендерер его игнорирует. Сценарий «icon + sub-icon + title» (например, иконка фичи + логотип OpenCode) не работал.
Условный рендер слотов (PR#144, ADR-061) уже поддерживает опциональность: передан `sub-icon` → рендерится; не передан → слот пропускается. Это доказано fixture-тестами (`render.optional.test.ts` на `tests/fixtures/cover.svg`, где sub-icon присутствует). Движок поддерживал sub-icon всегда — не хватало только объявления слота в продакшн-шаблоне.
## Решение
1. **Восстановлена ровно одна строка** slot-комментария `sub-icon` в `templates/cover.svg`:
`<!-- slot: name=sub-icon, x=412, y=600, w=200, h=200, fit=contain, recolor=accent -->`
(та самая, что удалил PR#197). Изменений в коде движка (`src/render.ts`) и команде `/cover` нет.
2. **Только `sub-icon`**`badge` и `subtitle` остаются удалёнными из продакшн-шаблона (вне scope PR#201). `/cover` (ADR-088) запрещает subtitle стандартом; badge не используется в Lime-стандарте.
3. **Условный рендер (PR#144) сохраняет одно-иконочный кейс побайтово идентичным**: `assets/cover.png` (`icon=opencode`, без `sub-icon`) → 18077 bytes, `cmp` → IDENTICAL. Рендер с двумя иконками (`icon=mic,sub-icon=opencode`) → 23171 bytes (больше, как ожидалось).
4. **Тесты не затронуты**: 58/58 проходят — тесты используют `tests/fixtures/cover.svg` (PR#197), а не продакшн-шаблон, поэтому изменение продакшн-шаблона их не ломает.
## Альтернативы
- **Не восстанавливать слот, убрать `sub-icon` из `/cover` (ADR-088)**: отвергнуто — `sub-icon` — полезный сценарий (иконка фичи + логотип проекта), и ADR-088 уже документирует его как optional. Убрать из команды = потерять функциональность, которую движок уже поддерживает.
- **Восстановить также `badge` и `subtitle`**: отвергнуто — вне scope issue #200. Lime-стандарт (ADR-088) запрещает subtitle; badge не используется. Восстановление мёртвых слотов вернёт проблему, которую решал PR#197.
- **Изменить `buildSvg` чтобы итерировать по `args.slots`, а не по шаблонным слотам**: отвергнуто — крупное архитектурное изменение движка ради одной строки. Текущая модель (шаблон объявляет слоты, рендерер их заполняет) чище и детерминированнее: шаблон = контракт на layout, args = данные.
## Последствия
- Частичный реверс ADR-087 (PR#197): продакшн `templates/cover.svg` снова объявляет `sub-icon` (наряду с `icon`). `badge` и `subtitle` остаются удалёнными — реверс точечный.
- Сценарий «icon + sub-icon» теперь работает в продакшн-рендере; одно-иконочный кейс побайтово идентичен (условный рендер PR#144).
- Тест-контракт (`tests/fixtures/cover.svg`) не изменён — продакшн и fixture снова расходятся по набору слотов (fixture имеет все 4 типа, продакшн — 2), но это намеренно: fixture тестирует движок, продакшн — конкретный бренд-стандарт.

View file

@ -0,0 +1,22 @@
---
pr: 201
title: "fix(cover): restore sub-icon slot in cover.svg"
---
## Что сделано
Восстановлена ровно одна строка slot-комментария `sub-icon` в `.opencode/draw-image/templates/cover.svg` (та самая, что удалил PR#197):
`<!-- slot: name=sub-icon, x=412, y=600, w=200, h=200, fit=contain, recolor=accent -->`
Изменений в коде движка (`src/render.ts`) и команде `/cover` нет — ситуативность (1 или 2 иконки) обеспечивается условным рендером слотов (PR#144). Структурных изменений нет (одна строка в существующем файле).
Проверено: 58/58 тестов проходят (тесты на `tests/fixtures/cover.svg`, не на продакшн-шаблоне); `assets/cover.png` (`icon=opencode`, без `sub-icon`) побайтово идентичен (18077 bytes, `cmp` → IDENTICAL); рендер с двумя иконками → 23171 bytes (больше, как ожидалось).
## Почему
PR#197 удалил slot-комментарий `sub-icon` из продакшн-шаблона, из-за чего переданный `sub-icon` молча отбрасывался — `buildSvg` итерирует по слотам, объявлённым в шаблоне, а не по `args.slots`. Команда `/cover` (PR#199) документирует `sub-icon` как optional и передаёт его в `draw-image` — возникала рассогласованность: команда обещает `sub-icon`, рендерер его игнорирует. Восстановление слота делает сценарий «icon + sub-icon + title» рабочим, а условный рендер (PR#144) сохраняет одно-иконочный кейс побайтово идентичным. Частичный реверс ADR-087 (только `sub-icon`; `badge`/`subtitle` остаются удалёнными — вне scope).
## Pending
— (после merge: memory-syncer может зафиксировать, что продакшн cover.svg снова объявляет sub-icon — project-map уже обновлён в этом PR)
## Watch out
- **Только `sub-icon` восстановлен**`badge` и `subtitle` остаются удалёнными из продакшн-шаблона (PR#197). Lime-стандарт (ADR-088) запрещает subtitle; badge не используется. Не путать с `tests/fixtures/cover.svg`, где все 4 типа слотов присутствуют (fixture тестирует движок, продакшн — бренд-стандарт).
- **Условный рендер (PR#144) — ключевая инварианта**: одно-иконочный кейс побайтово идентичен. Если будущий PR меняет `render.ts` или slot-parser, проверить `cmp assets/cover.png` не сломался.
- **`buildSvg` итерирует по шаблонным слотам, не по `args.slots`** — это контракт движка: шаблон объявляет layout (слоты), args — данные. Добавить новый слот в продакшн = добавить slot-комментарий в `templates/cover.svg` (как сделано здесь), а не менять движок.

View file

@ -70,7 +70,7 @@ opencode-config/
│ │ ├── render.mjs # SVG string → PNG via sharp (fontFiles: Geist TTF bundle) │ │ ├── render.mjs # SVG string → PNG via sharp (fontFiles: Geist TTF bundle)
│ │ ├── cli.ts # CLI entry: render <template> --title --slots --out [--template-dir dir]; temp SVG в os.tmpdir()/draw-image-${pid}.svg + finally rmSync (race-safe для параллельных рендеров) — PR#178, PR#197 (--template-dir) │ │ ├── cli.ts # CLI entry: render <template> --title --slots --out [--template-dir dir]; temp SVG в os.tmpdir()/draw-image-${pid}.svg + finally rmSync (race-safe для параллельных рендеров) — PR#178, PR#197 (--template-dir)
│ │ ├── brand.json # default palette slaid098 (base/surface/fg/muted/accent/line) │ │ ├── brand.json # default palette slaid098 (base/surface/fg/muted/accent/line)
│ │ ├── templates/cover.svg # 1024×1024 cover template (упрощён: только icon-слот 520×520 + {{title}} y=830; sub-icon/badge/subtitle удалены — PR#197, тесты на fixture) │ │ ├── templates/cover.svg # 1024×1024 cover template (icon-слот 520×520 + sub-icon-слот 200×200 [восстановлен PR#201, был удалён PR#197] + {{title}} y=830; badge/subtitle удалены — PR#197, тесты на fixture)
│ │ ├── fonts/ # Geist Sans TTF (Regular + Bold) bundled for sharp fontFiles │ │ ├── fonts/ # Geist Sans TTF (Regular + Bold) bundled for sharp fontFiles
│ │ ├── icons/lucide/ # 2007 Lucide SVG icons (synced from npm lucide-static via postinstall) │ │ ├── icons/lucide/ # 2007 Lucide SVG icons (synced from npm lucide-static via postinstall)
│ │ ├── brand-logos/ # brand SVG logos (opencode.svg — адаптированный логотип OpenCode 512×512 fill #ccff00, резолвится через draw-image slot icon=opencode) — PR#158 │ │ ├── brand-logos/ # brand SVG logos (opencode.svg — адаптированный логотип OpenCode 512×512 fill #ccff00, резолвится через draw-image slot icon=opencode) — PR#158