# ADR-060: SVG template renderer for on-brand covers

## Статус
Accepted (2026-07-29)

## Контекст
В продуктовых репо slaid098 нет ни одного cover-ассета. Обложки для slaid098.dev рисовались вручную или генерировались fallback-функцией (svgFallback в route.ts — HSL-градиент + буква, off-brand). Нужна утилита, которая берёт SVG-шаблон со слотами, подставляет палитру из brand.json + контент из args, рендерит PNG через sharp с bundled Geist TTF — always on-brand.

Архитектурный выбор: размещение в `.opencode/` (opencode plugin + render-движок) через volume bind-mount → видна во всех воркспейсах глобально. Отдельный package.json в `.opencode/draw-image/` (deps: sharp, lucide-static) — не загрязняет `.opencode/package.json` (используется для tools/*.ts).

## Решение
1. **opencode plugin** (`.opencode/tools/draw-image.ts`) — spawnSync wrapper вокруг `node cli.ts render ...`, возвращает `{ path, hash, status }`
2. **Слот-система** — HTML-комментарии в SVG-шаблоне парсятся в SlotSpec (name/x/y/w/h/fit/recolor/bg/border/radius). Контент резолвится: Lucide lookup → brand-logos lookup → file path
3. **Render pipeline** — buildSvg (assemble) → render.mjs (sharp → PNG 1024×1024, fontFiles: Geist TTF)
4. **Идемпотентность** — cover.meta.json с sha256(brand+template+args), skip при совпадении хеша
5. **Lucide-иконки** — npm-пакет lucide-static, postinstall sync в icons/lucide/, recolor stroke → accent
6. **Шрифты** — Geist Sans TTF bundled в fonts/, переданы в sharp через fontFiles (без этого fallback = off-brand)
7. **Тесты** — vitest (48: unit + integration + e2e) + pytest (5: tool wrapper via _ts_loader.mjs)

## Альтернативы
- **@resvg/resvg-js вместо sharp** — sharp уже используется в экосистеме, fontFiles опция покрывает потребность. resvg был бы отдельной dep.
- **Шрифты через fontconfig (системная установка)** — негибко, требует root в CI. Bundle TTF в fonts/ + fontFiles опция sharp — self-contained.
- **Шаблоны в коде (не SVG-файлы)** — менее гибко, требует правок кода для нового шаблона. SVG-файлы в templates/ — ноль правок кода.
- **Отдельный package.json vs расширить .opencode/package.json** — отдельный package.json изолирует deps (sharp тяжёлая native dep) от tools/*.ts.