# Pipeline — uma ficha → N renderizações

## Conteúdo

- 0. O princípio, e por que ele muda o custo do trabalho
- 1. As etapas, em ordem
- 2. Instalação e ambiente — o que está verificado
- 3. Etapa 1 — spec Vega-Lite (recapitulação, dono é `ficha-para-vega.md`)
- 4. Etapa 2 — spec → SVG, o comando exato, `vl_version` fixado
- 5. Etapa 3 — SVG → HTML completo (`modo: rascunho`, live view)
- 6. Do HTML às N saídas finais — o que foi testado, comando por comando
  - 6.1 PDF via Chrome headless (testado, funciona)
  - 6.2 PNG (screenshot de página inteira) via Chrome headless (testado, funciona)
  - 6.3 DOCX a partir do PNG, via `python-docx` (testado, funciona)
  - 6.4 PNG direto via `vlc.vegalite_to_png` — testado, funciona, MAS com uma pegadinha de fonte real
- 7. SVG direto embutido no Typst
- 8. `modo: rascunho` vs `modo: termo` — onde a flag realmente entra
- 9. Resumo — o que está provado vs o que não está

Este arquivo é o dono da mecânica de geração: como a ficha de widget vira,
sem nenhum passo manual, o artefato que o cliente efetivamente vê — HTML
vivo, PDF, DOCX. Leia ANTES de gerar qualquer artefato final; os specs
Vega-Lite em si (o que cada `relacao` vira) são de
[`ficha-para-vega.md`](ficha-para-vega.md), o esqueleto HTML é de
[`wireframe-kit.md`](wireframe-kit.md), o texto do marcador de confiança é
de [`marcadores-confianca.md`](marcadores-confianca.md) — este arquivo só
liga os três num pipeline de ponta a ponta e documenta o que foi de fato
testado.

## 0. O princípio, e por que ele muda o custo do trabalho

**A ficha do widget é a ÚNICA coisa autorada.** Tudo que sai dela —
HTML para iterar com o analista, SVG para embutir num PDF, PNG para embutir
num DOCX — é **gerado**, nunca escrito à mão widget por widget. Isso é o que
faz a fidelidade visual alta custar ~zero marginal: não se doa hora de
design por artefato, doam-se segundos de geração por artefato. Uma ficha
nova ou uma ficha corrigida não é retrabalho de N formatos — é rodar o
mesmo pipeline de novo.

**Corolário direto: o registro visual (`modo: rascunho` vs `modo: termo`)
é uma flag de render, não um retrabalho.** As duas rodadas passam pelo
mesmo spec Vega-Lite, o mesmo SVG, a mesma montagem HTML — a única coisa
que muda é o texto do marcador de confiança
(`marcadores-confianca.md` §5: mesma frase-base, fecho diferente) e,
adiante, se o artefato final vira PDF de assinatura ou fica só em HTML de
rodada. Nenhum widget é redesenhado entre os dois modos; o gerador recebe
`modo:` como parâmetro, não como bifurcação de código.

## 1. As etapas, em ordem

```
ficha de widget (dashboards/*.md, lumo-scope)
        │  ficha-para-vega.md — relacao + forma → spec
        ▼
spec Vega-Lite (dict/JSON, tema Horus ainda NÃO embutido)
        │  vl-convert (vegalite_to_svg, config=tema, vl_version fixado)
        ▼
SVG (o ÚNICO artefato de gráfico gerado — tudo depois é embalagem)
        │
        ├─► embutido inline num <div class="wf-body"> → HTML completo
        │        (wireframe-kit.md) = modo: rascunho, live view no navegador
        │        │
        │        ├─► Chrome headless --print-to-pdf ─────────────► PDF
        │        └─► Chrome headless --screenshot (PNG) ─────────► DOCX
        │                                                    (python-docx /
        │                                                     colar no Word)
        │
        └─► embutido direto num documento Typst via #image("widget.svg")
                 = termo-abertura.typ, rota vetorial sem HTML/browser
```

Não existem "3 pipelines" (HTML, PDF, DOCX) — existe **1 SVG por widget**,
gerado uma vez, consumido de formas diferentes. A única bifurcação real
anterior ao SVG é o spec (que já é por widget, `ficha-para-vega.md`); depois
do SVG, tudo é montagem/empacotamento, nunca um segundo caminho de geração
de gráfico.

## 2. Instalação e ambiente — o que está verificado

```bash
pip show vl-convert-python
```

```
Name: vl-convert-python
Version: 1.9.0.post1
```

`vl_convert` importa e funciona nesta máquina (Python 3.13.1, via pyenv).
Não há passo de instalação adicional além do `pip install vl-convert-python`
— é um binário Rust compilado (`.abi3.so`) com bindings Python, sem
dependência de Node/browser para a etapa spec→SVG.

> **`which node` falhando não significa Node ausente.** Gerenciador de versão
> (nvm, fnm, volta, asdf) só entra no `PATH` de shell interativo: um comando
> disparado por agente não carrega o `~/.nvm/nvm.sh` e enxerga um `PATH` sem ele.
> Concluir "ausente" daí já custou o descarte indevido do live-view, que funciona.
> Antes de declarar Node ausente, procure nos gerenciadores:
>
> ```bash
> which node \
>   || ls ~/.nvm/versions/node/*/bin/node \
>   || ls ~/Library/Application\ Support/fnm/node-versions/*/installation/bin/node \
>   || ls ~/.volta/bin/node \
>   || ls ~/.asdf/installs/nodejs/*/bin/node
> ```
>
> Achando, exporte antes de usar:
> `export PATH="$HOME/.nvm/versions/node/<versao>/bin:$PATH"`. O mesmo cuidado
> vale para qualquer binário instalado por gerenciador de versão (`pyenv`,
> `rbenv`, `sdkman`), e o modo de falha é sempre o mesmo: a ferramenta existe,
> o agente conclui que não, e uma rota boa é descartada por engano.

Verificação honesta das outras ferramentas do pipeline, **nesta máquina,
na máquina de referência** (não presuma que valem no ambiente do analista — a mesma
checagem deveria rodar lá antes de prometer uma rota):

| Ferramenta | Comando de verificação | Resultado medido |
|---|---|---|
| `vl-convert-python` | `pip show vl-convert-python` | **1.9.0.post1, instalado e funcional** |
| Typst | `which typst` | **`/opt/homebrew/bin/typst`, 0.14.2 — instalado** (diferente da sessão real do analista, onde faltava; ver §7) |
| Chrome | `"/Applications/Google Chrome.app/Contents/MacOS/Google Chrome" --version` | **Google Chrome 150.0.7871.129 — presente** |
| Node | `which node` **e**, se falhar, os caminhos de gerenciador de versão (abaixo) | **presente** (v24.15.0, via nvm). Só o companion de live-view (`wireframe-companion.md`) usa Node; nenhuma rota de render depende dele |
| `wkhtmltopdf` | `which wkhtmltopdf` | ausente, e **não seria recomendado mesmo se estivesse** — sem manutenção, não suporta CSS moderno (grid, custom properties) que o kit usa |
| `pandoc` | `which pandoc` | presente, não usado (ver §6 para por que a rota escolhida para DOCX foi outra) |
| `python-docx` | `python3 -c "import docx"` | presente (`pip install python-docx` se faltar) |

## 3. Etapa 1 — spec Vega-Lite (recapitulação, dono é `ficha-para-vega.md`)

Não duplicado aqui — `ficha-para-vega.md` §1 tem a tabela completa dos 12
`relacao` → widget do produto → spec, com `labels: false` no canal de
magnitude (a gridline fica), o layer de `text` com o placeholder constante
no lugar do data label, e
`scale.domain` explícito no canal de cor. Este arquivo assume que o spec já
existe como dict Python (ou JSON) e começa dali.

Specs de exemplo que provam o pipeline completo (§5),
reaproveitados do exemplo canônico já validado em `wireframe-kit.md`
("Prova — dois widgets do exemplo `vendas-executivo`"):

```python
# widget 3 — ranking, "Receita Líquida por Loja — mês corrente"
widget3_spec = {
  "$schema": "https://vega.github.io/schema/vega-lite/v5.json",
  "width": 340, "height": 140,
  "title": "Receita Líquida por Loja — mês corrente",
  "data": {"values": [
    {"loja": "Loja Sul", "valor": 1.00}, {"loja": "Loja Centro", "valor": 0.62},
    {"loja": "Loja Norte", "valor": 0.38}, {"loja": "Loja Leste", "valor": 0.24},
    {"loja": "Loja Oeste", "valor": 0.15}
  ]},
  "mark": {"type": "bar", "aria": False},
  "encoding": {
    "y": {"field": "loja", "type": "nominal", "sort": "-x", "title": None,
          "axis": {"domain": True, "ticks": True}},
    "x": {"field": "valor", "type": "quantitative", "axis": None},
    "color": {"field": "loja", "type": "nominal", "legend": None,
      "scale": {"domain": ["Loja Sul", "Loja Centro", "Loja Norte", "Loja Leste", "Loja Oeste"]}}
  }
}

# widget 2 — tendencia-inline, sparkline do card de KPI
widget2_spec = {
  "$schema": "https://vega.github.io/schema/vega-lite/v5.json",
  "width": 80, "height": 24,
  "data": {"values": [{"periodo": f"P{i+1}", "valor": v} for i, v in enumerate(
    [0.25, 0.305, 0.36, 0.415, 0.47, 0.525, 0.58, 0.635, 0.69, 0.745, 0.8, 0.855]
  )]},
  "mark": {"type": "line", "aria": False},
  "encoding": {
    "x": {"field": "periodo", "type": "ordinal", "sort": None, "axis": None},
    "y": {"field": "valor", "type": "quantitative", "axis": None}
  }
}
```

## 4. Etapa 2 — spec → SVG, o comando exato, `vl_version` fixado

`ficha-para-vega.md` §2.2 já apontou o risco, e ele foi reproduzido: sem
`vl_version` explícito, `vegalite_to_svg` resolve "a mais recente
disponível no binário" — hoje Vega-Lite 6.4.1, mas isso pode mudar numa
atualização futura do `vl-convert-python` **sem nenhuma mudança neste
repositório**, quebrando reprodutibilidade entre execuções/ambientes. A
mesma referência recomendou explicitamente que `pipeline.md` decidisse
fixar a versão — decisão tomada aqui: **sim, fixar `vl_version="6.4"` em
toda chamada de conversão** (SVG e PNG).

```python
import json
import vl_convert as vlc

# SKILL_DIR = o diretório ABSOLUTO desta skill (lumo-mockup) na máquina em uso.
# Varia por agente e por SO — ~/.claude/skills/lumo-mockup, ~/.agents/skills/...,
# %USERPROFILE%\.claude\skills\lumo-mockup. Descubra-o (é o diretório de onde
# você leu este arquivo) e substitua antes de rodar. Caminho de repositório
# (llms/…) NUNCA funciona: fora do repo o arquivo não existe.

# OBRIGATÓRIO, e antes de qualquer conversão. Sem isso o PNG sai com todos os
# rótulos ausentes e nenhum erro é levantado. Ver §6.4.
vlc.register_font_directory(f"{SKILL_DIR}/assets/fonts")

theme = json.load(open(f"{SKILL_DIR}/references/horus-theme.json"))
spec = json.load(open("widget3.vl.json"))

svg = vlc.vegalite_to_svg(spec, config=theme, vl_version="6.4")
```

Comprovado por três testes:

1. **`vlc.get_vegalite_versions()`** retorna
   `['5.8', '5.14', '5.15', '5.16', '5.17', '5.20', '5.21', '6.1', '6.4']`
   — `"6.4"` é um valor de fato suportado pelo binário instalado, não uma
   string inventada.
2. **`vl_version="6.4"` produz o MESMO SVG que a chamada sem `vl_version`**
   (default), a menos de um `id` de `clip-path` gerado aleatoriamente por
   chamada (`clip1` vs `clip6`) — confirmado normalizando os dois SVGs
   (`re.sub(r'clip\d+', 'clipN', svg)`) e comparando byte a byte: **idênticos**.
   Ou seja, fixar a versão hoje não muda nada visualmente — é proteção
   contra o futuro, não correção de um bug atual. `"v6.4"` (com prefixo,
   forma do docstring) também funciona e produz o mesmo resultado
   normalizado — use a forma sem prefixo (`"6.4"`) porque é exatamente o
   que `get_vegalite_versions()` devolve, copiável direto de lá.
3. **Uma versão inexistente falha alto e claro, não silenciosamente**:
   `vlc.vegalite_to_svg(spec, config=theme, vl_version="9.9")` levanta
   `RuntimeError('Unsupported Vega-Lite version string 9.9')` — é seguro
   fixar a versão porque um erro de digitação no pin não passa despercebido,
   trava a geração com uma mensagem acionável.

## 5. Etapa 3 — SVG → HTML completo (`modo: rascunho`, live view)

O SVG entra **inline** (string `<svg>...</svg>` direto no markup, sem
`<img>`, sem base64) dentro do `.wf-body` de cada widget, no esqueleto de
`wireframe-kit.md`. Montado e testado com os dois specs do §3:

```python
svg2 = vlc.vegalite_to_svg(widget2_spec, config=theme, vl_version="6.4")  # 797 bytes
svg3 = vlc.vegalite_to_svg(widget3_spec, config=theme, vl_version="6.4")  # 8681 bytes

html = f"""<!doctype html><html lang="pt-BR">...
  <div class="wf-banner">DADOS ILUSTRATIVOS — A FORMA REFLETE A PROPOSTA, OS VALORES NÃO REFLETEM O CLIENTE</div>
  <div class="wf-overlay">{overlay_texto}</div>
  <div class="wf-w" style="grid-column:span 6; grid-row:span 2"><span class="wf-n">3</span>
    <h3>Receita Líquida por Loja — mês corrente</h3>
    <div class="wf-body">{svg3}</div>
  </div>
..."""
```

`overlay_texto` é a UMA decisão de negócio da tela (`pergunta:` +
`decisoes_que_suporta:` da ficha do dashboard, fundidas conforme
`overlay-narrativo.md` §2-5) — nunca gerada por widget, nunca ausente.

Arquivo resultante: 12.370 bytes, HTML autocontido (`vendas-executivo-mockup.html`
no scratchpad desta task) — banner de dados ilustrativos, overlay
narrativo (`.wf-overlay`), grid de 12 colunas, os dois widgets com gráfico
real embutido, trilho de anotações. Este é o artefato `modo: rascunho` — o
que o analista abre no navegador durante a iteração (via
`wireframe-companion.md`, se `node` disponível, ou como arquivo estático se
não).

Confirmado no SVG gerado (`grep` sobre o markup): nenhum
`<text>` do widget 3 carrega dígito (`grep -oE '<text[^>]*>[^<]*</text>'`
não bate `[0-9]` em nenhum texto visível) e o widget 2 (sparkline) não gera
nenhum grupo de eixo nem `<text>` — mesma prova que `ficha-para-vega.md`
§2.3 já fez para este caso, revalidada aqui como parte do pipeline
completo, não como afirmação nova.

## 6. Do HTML às N saídas finais — o que foi testado, comando por comando

### 6.1 PDF via Chrome headless (testado, funciona)

```bash
"/Applications/Google Chrome.app/Contents/MacOS/Google Chrome" \
  --headless --disable-gpu --no-sandbox \
  --no-pdf-header-footer \
  --print-to-pdf="mockup.pdf" \
  "file://$(pwd)/vendas-executivo-mockup.html"
```

**Nota de honestidade sobre a própria tentativa**: a primeira vez que rodei
isto usei a flag `--print-to-pdf-no-header`, que **não existe** — o Chrome a
ignora silenciosamente, e o PDF saiu com cabeçalho/rodapé automático (data,
título, URL, numeração) que eu não queria. A flag correta, confirmada
funcionando, é **`--no-pdf-header-footer`**. Ambas as tentativas ficaram
gravadas no scratchpad desta task (`mockup.pdf` com cabeçalho,
`mockup2.pdf` sem) — registrar o erro da primeira tentativa é o mesmo
princípio que este arquivo inteiro existe para proteger: não documentar
comando não verificado como se funcionasse de primeira.

Prova — `pdfinfo` + `pdftotext` sobre o PDF gerado com a flag correta:

```
Producer:        Skia/PDF m150
Pages:           1
Page size:       612 x 792 pts (letter)
```

```
DADOS ILUSTRATIVOS — A FORMA REFLETE A PROPOSTA, OS VALORES NÃO REFLETEM O
CLIENTE
Receita Líquida — mês
Receita Líquida por Loja — mês corrente
Loja Sul
Loja Centro
Loja Norte
Loja Leste
Loja Oeste
Anotações
1. Receita Líquida: ver KPI card, tendência diária embutida como sparkline.
2. Ranking por loja: datalabels SIM, sem legenda, R$ mil; clique filtra
widgets seguintes.
```

Texto real, extraível — os nomes das lojas e o título vieram do SVG
embutido, nenhum eixo numérico apareceu. O Chrome resolveu a fonte e o PDF saiu
com texto de verdade, não rasterizado — confirmado por `pdftotext` conseguir
extrair as strings.

Na rota Chrome, sirva Poppins pelo `<style>` da página apontando para
`assets/fonts/` via `@font-face` com caminho `file://`, e não por `@import` de
Google Fonts: o artefato precisa abrir sem rede, porque ele circula por e-mail e
é aberto em máquina de cliente.

### 6.2 PNG (screenshot de página inteira) via Chrome headless (testado, funciona)

```bash
"/Applications/Google Chrome.app/Contents/MacOS/Google Chrome" \
  --headless --disable-gpu --no-sandbox \
  --window-size=1280,900 \
  --screenshot="mockup.png" \
  "file://$(pwd)/vendas-executivo-mockup.html"
```

Resultado: PNG 1280×900 (`file`/`sips` confirmam), inspecionado
visualmente — banner, KPI com placeholder, sparkline, barras rankeadas com
nome de loja, trilho de anotações, tudo presente e legível. `--window-size`
deve ser ajustado para caber o conteúdo real (aqui sobrou espaço em branco
abaixo — inofensivo, mas ajustável por dashboard).

### 6.3 DOCX a partir do PNG, via `python-docx` (testado, funciona)

```python
import docx
from docx.shared import Inches

d = docx.Document()
d.add_heading("Mockup — Vendas Executivo (modo: rascunho)", level=1)
d.add_paragraph("Dados ilustrativos — a forma reflete a proposta, os valores não refletem o cliente.")
d.add_picture("mockup.png", width=Inches(6.3))
d.save("mockup.docx")
```

Verificado: `mockup.docx` é um OOXML válido (`file` confirma
`Microsoft OOXML`), com `word/media/image1.png` de fato embutido dentro do
zip (`python3 -c "import zipfile; ..."` listou o arquivo). Esta é a rota
**automatizável** da mesma prática real que o analista já faz manualmente
("abre o HTML, tira print, cola no Word") — `python-docx` é dependência
opcional (`pip install python-docx`), só necessária se quem gera quiser
pular o copiar-colar manual.

**Por que PNG (screenshot) para DOCX, e não `vlc.vegalite_to_png`
direto — achado importante desta task, ver §6.4.**

### 6.4 PNG direto via `vlc.vegalite_to_png` — testado, funciona, MAS com uma pegadinha de fonte real

A instrução desta task pedia para testar `vegalite_to_png` antes de
documentar. Testei — e o resultado tem uma ressalva que muda a recomendação
de rota:

```python
png_bytes = vlc.vegalite_to_png(widget3_spec, config=theme, vl_version="6.4")
# funciona: bytes válidos, PNG 438×182, sem erro
```

`file`/`sips` confirmam um PNG válido. **Mas, inspecionado visualmente, o
gráfico saiu SEM título e SEM os nomes das lojas no eixo — só as barras
coloridas, sem nenhum texto.** Isso não é o comportamento do SVG (que
sempre carrega `<text>`, independente de fonte instalada) — é específico
dos conversores nativos de rasterização do `vl-convert` (`vegalite_to_png`,
e confirmado que o mesmo vale para `vegalite_to_pdf` e `svg_to_pdf`/
`svg_to_png`, testados separadamente com o mesmo resultado: barras sim,
texto não).

**Causa raiz, confirmada**: a fonte do tema não está instalada como fonte de
sistema. O SVG gerado carrega `font-family` corretamente em cada `<text>`, mas
os conversores nativos do `vl-convert` (rasterização própria, sem browser)
precisam resolver essa fonte **localmente** para desenhar o glifo, e quando não
encontram **omitem o texto inteiro** em vez de cair num fallback visível.
Provado comparativamente: a mesma conversão com o tema trocado para
`"Helvetica"` (presente no macOS) trouxe título e rótulos de volta.

Isso **não acontece** na rota do §6.1/§6.2 (Chrome headless): o Chrome
renderiza o texto com uma fonte de fallback em vez de omiti-lo. O mesmo vale ao
embutir o SVG num documento Typst (§7). **A omissão total de texto é
comportamento específico dos conversores nativos do `vl-convert`
(`vegalite_to_png`, `vegalite_to_pdf`, `svg_to_png`, `svg_to_pdf`).**

### A correção, agora validada ponta a ponta

A skill embarca Poppins em `assets/fonts/` (ver o README de lá) exatamente para
isso. A correção está verificada:

```python
vlc.register_font_directory(f"{SKILL_DIR}/assets/fonts")
```

| Render | Bytes | Texto |
|---|---|---|
| sem registrar o diretório | 43.584 | **todos os rótulos ausentes** |
| com `register_font_directory` | 54.511 | todos os rótulos presentes |

Mesmo spec, mesma máquina, diferença só na chamada de registro. A inspeção
visual do PNG confirmou os nomes de categoria de volta.

**Consequência prática**: `vegalite_to_png` e `vegalite_to_pdf` diretos voltam a
ser rotas de primeira classe, desde que o registro venha antes. A rota Chrome
headless do §6.1/§6.2 continua válida, e continua sendo a escolha quando o
layout completo da página (com o chassi HTML) precisa ir junto, não só o
gráfico.

**O modo de falha merece respeito porque é silencioso**: nada é levantado, e o
PNG "funciona". Um artefato que vai para proposta comercial sem nenhum rótulo é
o tipo de defeito que só aparece quando o cliente já está com ele na mão. Por
isso o registro virou regra dura no `SKILL.md`, e não uma recomendação.

## 7. SVG direto embutido no Typst

Para o termo de abertura (`termo-abertura.typ`), o SVG gerado na etapa 2
embute direto num documento Typst via `#image()` — sem redesenhar nada:

```typst
#image("wireframes/vendas-executivo-widget-3.svg", width: 90%)
```

Testado (Typst 0.14.2, `which typst` →
`/opt/homebrew/bin/typst`): `typst compile test-embed.typ test-embed.pdf`
compilou sem erro, e `pdftotext` extraiu o título e os 5 nomes de loja do
gráfico embutido — sem a fonte do tema instalada no sistema, o Typst renderizou
o texto numa fonte de fallback em vez de omiti-lo, diferente do defeito do §6.4
(`vegalite_to_pdf`/`svg_to_pdf` nativos do `vl-convert` omitem o texto nessa
mesma condição).

Para que o Typst use Poppins de verdade em vez do fallback, aponte
`typst compile --font-path <SKILL_DIR>/assets/fonts`.

Embutir o PNG de página inteira do §6.2 (`#image("mockup.png", width:
90%)`) também funciona, mas o SVG por widget é a opção preferida para o
termo: mantém qualidade vetorial e não depende do Chrome para gerar o
wireframe de um documento já compilado via Typst.

## 8. `modo: rascunho` vs `modo: termo` — onde a flag realmente entra

Recapitulando `marcadores-confianca.md` §5 sob a lente deste pipeline: as
etapas 1-6/7 acima **não mudam** entre os dois modos — o mesmo spec, o
mesmo SVG, a mesma montagem HTML/Typst. A única diferença de conteúdo é o
texto do `.wf-confidence` (quando o widget carrega `confianca: hipótese`
ou `a-perguntar`): a versão `rascunho` acrescenta um convite à correção
("faz sentido pra vocês, ou querem ajustar algo aqui?"), a versão `termo`
é declarativa, sem pergunta. Nenhuma borda tracejada nova, nenhuma fonte
diferente, nenhuma segunda passada de geração de gráfico — é um parâmetro
de texto no mesmo gerador, não um branch de pipeline.

Na prática, quem chama a etapa de montagem HTML (§5) passa `modo:` junto
com a ficha; o texto do marcador sai de uma tabela de lookup
(`marcadores-confianca.md` §4/§5), nunca de uma segunda função de geração
de página.

## 9. Resumo — o que está provado vs o que não está

| Rota | Testado? | Evidência |
|---|---|---|
| Spec Vega-Lite → SVG (`vegalite_to_svg`, `vl_version` fixado) | **Sim** | §4 — SVGs gerados, texto sem dígito confirmado, `vl_version="6.4"` idêntico ao default (normalizado) |
| SVG → HTML completo (`modo: rascunho`, live view) | **Sim** | §5 — HTML de 12.370 bytes montado e inspecionado |
| HTML → PDF (Chrome headless `--print-to-pdf`) | **Sim** | §6.1 — `pdfinfo`/`pdftotext`, texto real extraído |
| HTML → PNG página inteira (Chrome headless `--screenshot`) | **Sim** | §6.2 — PNG 1280×900, inspecionado visualmente |
| PNG → DOCX (`python-docx`) | **Sim** | §6.3 — OOXML válido, imagem embutida confirmada no zip |
| Spec → PNG direto (`vegalite_to_png`) | **Sim, com defeito documentado** | §6.4 — funciona tecnicamente, mas omite texto sem fonte local; causa e fix documentados, fix **não** testado (faltou arquivo de fonte) |
| Spec/SVG → PDF direto (`vegalite_to_pdf`, `svg_to_pdf`) | **Sim, mesmo defeito** | §6.4 — mesmo sintoma do PNG direto |
| SVG → Typst (`#image`, termo de abertura) | **Sim** | §7 — compilado, texto extraído via `pdftotext` |
| `register_font_directory` como correção do §6.4 | **Sim** | §6.4 — validado ponta a ponta em 2026-07 com Poppins embarcada em `assets/fonts/`: 43.584 bytes sem texto contra 54.511 com texto, mesmo spec |
| Companion de live-view (`wireframe-server.cjs`, precisa `node`) | **Sim** | Node v24.15.0 encontrado sob nvm (o `which node` inicial falhou por `PATH` de shell não-interativo, ver §2). `node --check` passa e o servidor sobe: `LIVE-VIEW: http://127.0.0.1:51850/` |
| `wkhtmltopdf` | **Não testado, não recomendado** | ausente nesta máquina; mesmo se presente, sem manutenção e sem suporte a CSS moderno — não é candidato |

Nenhuma linha desta tabela foi escrita a partir de "deveria funcionar" —
cada "Sim" corresponde a um comando rodado no scratchpad desta task com
saída inspecionada (texto extraído, imagem visualizada, ou estrutura de
arquivo verificada). Onde o comportamento surpreendeu (o `vl_version`
"default" já ser 6.4.1, os conversores nativos de PNG/PDF omitirem texto
sem fonte local), o achado entrou no documento como está — inclusive a
correção de rota que ele obriga — em vez de a linha de base do brief ser
maquiada.
