Lição 24 · Fusão v3 · @alembic/design + @alembic/docs ← índice
Alembic × Hermes — O Curso de Fusão v3 · Percepção & Fábrica (21–25)

@alembic/design + @alembic/docs: a marca como dado, o gate como função

A dupla que dá CARA ao motor: @alembic/design guarda o design-system Warm-Neutral como um preset TIPADO (copiado verbatim de docs/design-system.html) e renderers puros — renderHtmlPage emite o shell canônico dos cursos. Em volta, um portão de governança de verdade: pnpm detect roda o impeccable no CI, e um teste do próprio repo prova que o artefato LIGHT sai com zero achados. Ao lado, @alembic/docs transforma uma pasta de Markdown em site estático — com um sanitizador de URL que fecha a porta do XSS.

1

O tema é DADO validado, não CSS solto

packages/design/src/themes.ts declara o WARM_NEUTRAL como um ThemePreset de @alembic/contracts — e o comentário de cabeçalho crava a regra de proveniência: "Every token value below is copied VERBATIM from the live :root block of docs/design-system.html … these values are what the impeccable CI detector keys off, so a drift here is a brand-governance bug" (themes.ts:10–19). Paleta, tipografia (serif/sans/mono) e espaçamento viram tokens nomeados; a paleta DARK é um segundo conjunto com os MESMOS nomes (themes.ts:59–74), emitido como override.

Dois tokens contam a história do gate: o accent de texto pequeno em fundo claro é clay-ink: #BE4019 — não o clay: #E75533 (themes.ts:27–28) — e o cinza secundário é g500: #6B6A63 (themes.ts:35): os valores foram calibrados para passar contraste AA sob o detector. E pickTheme nunca lança: id desconhecido cai no default, e o preset escolhido ainda é validado pelo themePresetSchema antes de sair — registry corrompido vira Err, não render quebrado (themes.ts:96–107).

Pense como… o manual de identidade visual de uma marca — mas compilável: em vez de um PDF que o estagiário esquece, um objeto tipado que o CI confere. Onde a analogia quebra: manual de marca não tem safeParse — este tem, e um hex fora do lugar quebra o build, não a estética.

2

renderHtmlPage: um preset entra, o shell canônico sai

produto · 0007-factory-forge-cursos · s2

packages/design/src/render.ts é 100% puro: "same preset in, same string out, with no Date.now() / Math.random()" (render.ts:5–7). renderThemeCss achata os tokens num bloco :root { --nome: valor; } (render.ts:50–55); renderDarkThemeCss emite o override :root[data-theme="dark"] a partir da paletteDark (render.ts:66–73); e renderHtmlPage monta o documento completo com o shell canônico do curso: corpo 20.5px, h1/h3 serifadas, h2 mono-uppercase, toggle de dark embutido, camada de animação com guarda de reduced-motion e sidebar opcional com scrollspy (render.ts:133+; o CSS emitido em :229–312). Tudo interpolado passa por escapeHtml (render.ts:23–29).

O detalhe anti-bug: darkMode default é true, MAS só liga se o preset realmente carrega paletteDark — senão o toggle trocaria o data-theme sem tokens escuros atrás (render.ts:137–141). E com darkMode:false o documento é a marca LIGHT pura — "passes impeccable detect with zero findings" (render.ts:130–132) — sem NENHUMA maquinaria de toggle no HTML (render.ts:166–176).

Pense como… uma prensa tipográfica com uma única matriz de chumbo: troque a matriz (o preset) e TODA página futura muda junto; a página já impressa nunca muda sozinha. Onde a analogia quebra: esta prensa imprime duas edições da mesma matriz — a de arquivo (LIGHT, que o fiscal confere) e a de leitura (com o toggle) — e as duas saem da MESMA passada.

Honestidade de escopo: o registry de temas hoje tem UM preset — THEME_REGISTRY = { 'warm-neutral': WARM_NEUTRAL } (themes.ts:86–88); o próprio comentário marca skins alternativas como follow-up aditivo gated em gosto do founder (themes.ts:82–85). O plural do tipo existe; o catálogo, ainda não.
WARM_NEUTRAL pickTheme() valida themes.ts:96–107 renderHtmlPage :root tokens + shell canônico 20.5px · serif h1/h3 · mono h2 puro: mesmo preset ⇒ mesma string darkMode:false marca LIGHT pura — zero maquinaria dark impeccable detect ⇒ [] (gate-clean, provado por teste) darkMode:true shell dos cursos: toggle + paletteDark override só liga se o preset TEM paletteDark (render.ts:137–141) a mesma função, dois artefatos: o LIGHT é o que o GATE governa; o toggle é o que o LEITOR usa
design — preset validado → renderer puro → o artefato de governança (LIGHT) e o artefato de leitura (toggle).
3

O impeccable-gate: pnpm detect, docs/*.html, CI

produto · 0004-swarm-harness-gates · s1

O portão é curto e literal: o script raiz é "detect": "impeccable detect docs/*.html" (package.json:17) e o CI o roda como passo próprio — "Design gate (impeccable detect): pnpm detect" (.github/workflows/ci.yml:44–45). Repare no glob: docs/*.html pega os HTML do PRIMEIRO nível de docs/ — subpastas não entram no gate (o glob de um nível não desce; um HTML novo em docs/sub/ nasce FORA da governança até alguém promovê-lo).

1
preset no registry hoje (themes.ts:86–88)
0
findings exigidos pelo teste do gate (findings = [])
2
artefatos da mesma função: LIGHT (gate) + toggle (leitor)
1
nível de glob no gate: docs/*.html (package.json:17)

E o pacote de design vigia a si mesmo: packages/design/src/impeccable-gate.test.ts resolve o binário com which impeccable (skip honesto se ausente), renderiza um renderHtmlPage(warm-neutral, darkMode:false) num tmp e exige detect --jsonfindings = [] — ZERO achados. O comentário do teste explica o porquê da variante LIGHT: "The detector is theme-naive (it statically pairs every declared text colour with every declared background, so a two-theme file shows false cross-theme low-contrast). It therefore governs the LIGHT brand only" (impeccable-gate.test.ts:40–44); o contraste do dark é verificado theme-aware em render.test.ts.

O caso operacional: dezenas de achados → verdeEste gate não nasceu verde. No episódio real que calibrou os tokens (histórico de sessão do projeto, 2026-05 — o número exato, 78 achados numa passada, vem desse histórico e não é derivável do repo de hoje), a suíte docs/ acumulou dezenas de findings e foi trazida a zero SEM afrouxar o detector: texto clay pequeno migrou para clay-ink #BE4019, o cinza de apoio para g500 #6B6A63, corpo mínimo ≥12px, títulos sem pular nível. A prova viva dessas escolhas está nos próprios tokens (themes.ts:14–15, :28, :35) e no fato de o CI rodar o gate a cada push (ci.yml:44–45).
Por que o teste do gate renderiza com darkMode:false antes de exigir zero findings do impeccable?
Correto: o detector pareia estaticamente TODA cor de texto declarada com TODO fundo declarado — num arquivo de dois temas, o token escuro é cruzado com o fundo claro e vice-versa, gerando falsos low-contrast (impeccable-gate.test.ts:40–44). Por isso o gate governa a marca LIGHT; o contraste dark é validado theme-aware em render.test.ts.
4

@alembic/docs: Markdown → site, com a porta do XSS fechada

alembic docs <src> <out> (CLI em apps/cli/src/commands.ts:4233–4266) valida que a origem é um diretório e chama generateSite (packages/docs/src/site.ts:62–94): varre a árvore RECURSIVAMENTE por .md/.markdown (site.ts:26–38), extrai o título do primeiro # heading (site.ts:40–43), converte cada arquivo com markdownToHtml, espelha a estrutura de pastas em .html e fecha com um index.html listando todas as páginas (site.ts:86–91).

O conversor (packages/docs/src/markdown.ts) cobre headings, parágrafos, listas, fenced code, inline code, bold/itálico e links — e é no link que mora a segurança: sanitizeUrl é um allow-list de schemes (http:, https:, mailto:; relativo/âncora passa) e TUDO mais vira '#'. A defesa é mais funda que um startsWith: caracteres fora do ASCII imprimível são STRIPADOS antes da detecção de scheme, porque o browser os ignora dentro de um scheme — jAvAsCrIpT:, java\tscript: e variantes com newline embutido colapsam todos em '#' (markdown.ts:9–27).

src/**/*.md varredura RECURSIVA site.ts:26–38 markdownToHtml título do 1º heading escapeHtml em tudo sanitizeUrl markdown.ts:9–27 http/https/mailto out/**/*.html + index javascript:/data:/vbscript: ⇒ '#' ' jAvAsCrIpT:' e 'java\tscript:' também: chars não-imprimíveis são stripados ANTES
docs — cada href passa pelo losango do sanitizeUrl: allow-list explícito, evasões de case/whitespace colapsadas em '#'.
Um .md malicioso traz [click](jAvA\tScRiPt:alert(1)). O que o site gerado exibe no href?
Correto: sanitizeUrl remove tudo fora de [!-~] ANTES de detectar o scheme (o browser ignora esses chars dentro de um scheme, então java\tscript: executaria!), baixa para minúsculas e só deixa passar http/https/mailto — o resto vira '#' (markdown.ts:9–27).
5

Quem consome a dupla (e por quê isso importa)

A prova de que o tema-como-dado paga o preço da abstração está em quem o importa: packages/coda/src/course.ts — o gerador do curso visual-teach de alembic course — renderiza suas páginas SOBRE @alembic/design (prova: grep -rln '@alembic/design' packages/*/src apps/*/src lista coda/src/course.ts, contracts/src/theme.ts e a CLI). Ou seja: produto e cursos saem da MESMA função pura, do MESMO preset — quando um token muda no doc vivo, muda em TUDO que renderiza, e o gate confere.

ConsumidorO que usaEfeito
coda/course.tsrenderHtmlPage + WARM_NEUTRALcurso byte-estável com a cara da casa
contracts/theme.tsthemePresetSchemaa FORMA do preset é contrato público
apps/clidesign + docs (runDocs)alembic course · alembic docs
CI (pnpm detect)o artefato em docs/*.htmlgovernança a cada push (ci.yml:44–45)
Contraste honesto entre os dois pacotes: design é 100% puro (zero IO — render.ts:4–7) e docs é o oposto deliberado: generateSite lê e escreve disco diretamente (site.ts:62–94) — é uma ferramenta de build, não código de biblioteca do motor; seu único importador é a própria CLI (prova: grep -rln '@alembic/docs' packages/*/src apps/*/src).
CAMADA TÉCNICA — comandos copy-paste

Verificação $0 da dupla e do gate:

# os tokens verbatim + a regra de proveniência do tema
sed -n '9,40p;59,76p' packages/design/src/themes.ts

# o renderer puro e a trava darkMode-exige-paletteDark
sed -n '124,148p' packages/design/src/render.ts

# o gate: script raiz, passo de CI e o teste que exige zero findings
grep -n '"detect"' package.json
grep -n -A1 'Design gate' .github/workflows/ci.yml
sed -n '36,75p' packages/design/src/impeccable-gate.test.ts

# rodar o gate localmente (read-only; exit 0 = suíte verde)
pnpm detect

# gerar um site de docs a partir de qualquer pasta de .md
alembic docs docs/agents /tmp/agents-site --title "Agent docs"

# o sanitizador de URL e suas evasões cobertas
sed -n '9,27p' packages/docs/src/markdown.ts
O que levar desta lição
Pergunta de acompanhamento sugerida: "se o glob do gate é docs/*.html, o que garante a qualidade de um HTML novo em docs/sub/?" — hoje, nada além de disciplina: promova-o ao primeiro nível ou estenda o glob. Próxima lição: a dupla de OBSERVAR o motor rodando — TUI ASCII no terminal, cockpit web sobre os run-dirs e o servidor HTTP+SSE do harness.