diff --git a/.agents/skills/graficos-dados/SKILL.md b/.agents/skills/graficos-dados/SKILL.md new file mode 100644 index 00000000..133a942f --- /dev/null +++ b/.agents/skills/graficos-dados/SKILL.md @@ -0,0 +1,122 @@ +--- +name: graficos-dados +description: 'Transforma números que a comunicação recebeu (xlsx/csv/md/txt/texto colado) num gráfico PNG pronto para o Instagram — 1080×1350 feed, 1080×1080 quadrado ou 1080×1920 stories — com a paleta Jorge Solla, título-manchete, um único destaque de cor e guardrails de correção; dado ambíguo vira pergunta, nunca invenção.' +--- + +# Gráficos de dados para Instagram (C191) + +Entrega **um gráfico PNG** por invocação, no tamanho do Instagram e com a cara +do mandato, a partir do dado **como ele veio** — planilha, CSV, markdown ou +texto solto colado no comando. A skill lê, entende a **relação** dos dados +(comparação, tempo, parte-de-um-todo, número único), **propõe o tipo** correto, +gera a peça com **título-manchete** e **um destaque vermelho**, e deixa a +comunicação transformar o gráfico no material final (legenda, carrossel, +contexto). É a **base visual confiável**, não a peça acabada. + +O template visual é o artefato aprovado +`docs/plans/graficos-dados-instagram-ui-design.html`; o renderer +(`scripts/lib/graficosInstagramRender.mjs`) porta esse design classe-a-classe. +**Nunca invente estrutura visual** — o dono é o `designer`. + +## Quando usar + +- A comunicação pede "um gráfico disso", "transforma essa planilha em post", + "faz um gráfico para o Instagram" — e fornece os números (arquivo ou colado). +- **Não** use para: limpar/interpretar estatisticamente os dados, escrever a + legenda/carrossel, publicar/agendar, gerar dossiê/relatório em PDF (irmãos + C163/C186/C187) ou montar dashboard interativo. A skill **representa** o dado; + não o interpreta. + +## Fluxo + +1. **Receba o dado.** Texto colado → grave em + `data/graficos-instagram/-input.txt` (o diretório é gitignored) e aponte + com `--in=`. Arquivo → aponte o caminho direto (`xlsx`, `xls`, `csv`, `tsv`, + `md`, `txt`). +2. **Inspecione antes de desenhar:** + ```bash + node scripts/build-chart-from-data.mjs --in= --inspect + ``` + Devolve `{ rows, format, issues }` — a matriz parseada e os problemas. +3. **Decida o tipo e a copy.** A relação dos dados manda: + - **ranking / comparação → `bar`** (barras horizontais, ordenadas pelo valor, + rótulo direto e valor na ponta); + - **poucos períodos (≤4) → `column`** (base zero comum); + - **série de tempo contínua → `line`** (ponto final destacado); + - **uma única medida → `anchor`** (número grande + frase que explica). + Ambíguo (mais de um tipo plausível) → **pergunte**; não escolha no escuro. + Escreva a **manchete = takeaway** (não "Gráfico de X"), subtítulo opcional e a + **fonte** (viaja dentro da imagem). +4. **Gere o PNG:** + ```bash + node scripts/build-chart-from-data.mjs --in= \ + --headline="Um território concentra o maior resultado do período" \ + --subtitle="Valores de 2022" \ + --source="TSE 2022" --type=bar --size=feed \ + --highlight="Ilhéus" + ``` + Saída default: `docs/research/graficos-instagram/--.png` + (gitignored) + o `chart-spec.json` em `data/graficos-instagram/`. `--size`: + `feed` (1080×1350, default), `square` (1080×1080), `story` (1080×1920, com + faixas seguras de 250px no topo e na base). `--out=` sobrescreve o caminho. +5. **Confira e ajuste.** A pessoa confere o PNG; ajuste tipo/rótulo/manchete/ + tamanho/`--highlight` e regenere. **Replay** sem reparse: `--spec=`. +6. **Entregue o gráfico.** A comunicação finaliza a peça. O dado não persiste. + +## Contrato do `chart-spec.json` + +```json +{ + "chartType": "bar", + "size": "feed", + "headline": "Um território concentra o maior resultado do período", + "subtitle": "Valores de 2022", + "source": "TSE 2022", + "note": "valores arredondados", + "highlight": "Ilhéus", + "rows": [{ "label": "Ilhéus", "value": 84 }] +} +``` + +`chartType` ∈ `bar | column | line | anchor`; `highlight` é o rótulo que recebe o +único vermelho `#c51414` (default: o maior valor). + +## Guardrails (fail-closed) + +O builder **recusa** em vez de desenhar algo enganoso: + +- **Barras e colunas partem do zero**; valor negativo é recusado; sem 3D/eixo + truncado. +- **Até 7 pontos** por gráfico; em **stories**, o ranking comporta **até 5** + pontos (a adaptação vertical reduz um ponto, conforme o design aprovado). + Acima disso, resuma as categorias e explique. +- **Número-âncora:** a manchete é o takeaway (não repete o número) e o valor + aparece uma vez, grande, seguido da frase que explica. +- **Pizza não é gerada no v1** — pedido `pie` é recusado com sugestão de barras. +- **Dado faltando/ambíguo → pergunta, nunca completa.** O builder sai com + `{ needsQuestion: true, issues: [...] }` e não gera imagem. +- **Sem fonte, sem peça:** `--source` é obrigatório. +- Legibilidade: headline ≥48px e rótulos ≥30px no canvas 1080; contraste ≥4.5:1 + no texto e ≥3:1 nas marcas; **cor nunca é a única pista** (o destaque tem + valor e posição). +- **Dado de campanha nunca sai da máquina:** render 100% local, sem MCP/serviço + remoto por default. +- **Nada é commitado:** entrada, `chart-spec` e PNG vivem em + `data/graficos-instagram/` e `docs/research/graficos-instagram/`, ambos + gitignored (repo público). + +## Troubleshooting + +- **"dado ambíguo ou faltando"** → leia `issues` no JSON de saída: linha sem + rótulo, valor não numérico, rótulo repetido. Pergunte à pessoa; não invente. +- **"N pontos (> 7)"** → agrupe/remova categorias com a pessoa. +- **"pizza não é gerada"** → ofereça barras horizontais. +- **Número pt-BR:** `1.234,56` é lido como 1234,56; `1,5` como 1,5. +- **PNG grande:** a guarda de 8MB falha fechado; simplifique o gráfico. + +## Referências + +- Design aprovado: `docs/plans/graficos-dados-instagram-ui-design.html`. +- Plano de intenção: `docs/plans/graficos-dados-instagram.md`; impl: + `docs/plans/graficos-dados-instagram-impl.md`. +- Irmãos (mesmo padrão skill + command + builder local): C186/C187. diff --git a/.gitignore b/.gitignore index 61598904..44ce0c79 100644 --- a/.gitignore +++ b/.gitignore @@ -57,6 +57,7 @@ next-env.d.ts .env /media +/reelMedia # TSE open-data zip cache (pnpm db:seed:tse) /data/tse/ @@ -95,6 +96,10 @@ next-env.d.ts # C190 — dossiê Solla por tema/área + boletim modelo: same rule (C186). /data/dossie-solla-tema/ /docs/research/dossie-solla-tema/ +# C191 — Instagram data charts: raw input + chart-spec intermediates + final +# PNG/SVG carry campaign-internal data; the public repo never commits them. +/data/graficos-instagram/ +/docs/research/graficos-instagram/ # Local-only sensitive material (agents may read; never commit content) /private/** diff --git a/.opencode/commands/graficos-dados.md b/.opencode/commands/graficos-dados.md new file mode 100644 index 00000000..2e20f46e --- /dev/null +++ b/.opencode/commands/graficos-dados.md @@ -0,0 +1,9 @@ +--- +description: Gera um gráfico PNG pronto para o Instagram (1080×1350 feed, 1080×1080 quadrado ou 1080×1920 stories) a partir de dados em xlsx/csv/md/txt/texto colado, na paleta Jorge Solla, com título-manchete e um único destaque de cor +--- + +Carregue a skill `graficos-dados` (ferramenta de skills, nome exato) e siga o fluxo dela de ponta a ponta: leia os `$ARGUMENTS` (arquivo apontado ou números colados no comando), inspecione a matriz com `--inspect`, classifique a relação dos dados (comparação, tempo, número único), proponha o tipo e a manchete/fonte, gere o PNG no tamanho pedido com o builder local (`node scripts/build-chart-from-data.mjs`) e confira com a pessoa. A fonte canônica é `.agents/skills/graficos-dados/SKILL.md` — não a transcreva nem recrie o fluxo. + +Uso: `/graficos-dados ` — ex.: `/graficos-dados data/graficos-instagram/votos.csv` ou `/graficos-dados Ilhéus 84, Itabuna 68, Porto Seguro 53`. Dado ambíguo/faltando → pergunte; nunca invente. + +$ARGUMENTS diff --git a/docs/changelog/2026-09-18-c191-graficos-instagram.md b/docs/changelog/2026-09-18-c191-graficos-instagram.md new file mode 100644 index 00000000..f28bf097 --- /dev/null +++ b/docs/changelog/2026-09-18-c191-graficos-instagram.md @@ -0,0 +1 @@ +**C191 — Gráficos de dados para Instagram (skill de comunicação) (2026-09-18):** a comunicação agora transforma números como eles vieram (`xlsx`/`csv`/`md`/`txt`/texto colado) num **gráfico PNG pronto para o Instagram**, na paleta Jorge Solla e no formato do canal. (1) **Superfície:** skill `.agents/skills/graficos-dados/SKILL.md` + command `/graficos-dados` (sem subagente — a etapa semântica cabe no agente principal) e o entry `scripts/build-chart-from-data.mjs` (`--in`/`--inspect`/`--spec` replay/`--headline`/`--source`/`--type`/`--size`/`--highlight`). (2) **Núcleo puro** `scripts/lib/chartData.mjs`: parse dos formatos com `xlsx`/`csv-parse` (já devDeps, zero dependência nova), classificação da relação dos dados (ranking→`bar`, poucos períodos→`column`, série→`line`, medida única→`anchor`) e guardrails **fail-closed** (base zero, ≤7 pontos — 5 em stories, pizza recusada, `--source` obrigatório, nada de valor negativo); dado faltando/ambíguo vira pergunta, nunca invenção. (3) **Dono do desenho** `scripts/lib/chartPrimitives.mjs`: os primitivos SVG do dossiê (`barChart`/`valueList`/`columnChart`/`stackedColumnChart` + `CHART_COLORS`, extraídos byte-idênticos de `dossieRender.mjs`, golden pin) mais o `lineChart` e a escala `proportionalPercent`; **sem segundo renderer** ("edite o dono"). (4) **Renderer** `scripts/lib/graficosInstagramRender.mjs` porta classe-a-classe o hi-fi aprovado (`docs/plans/graficos-dados-instagram-ui-design.html`) em 3 canvases — `1080×1350` feed (default), `1080×1080` quadrado e `1080×1920` stories com faixas seguras de 250px derivadas de `SIZES`; título-manchete, um único destaque `#c51414`, fonte e lockup provisório dentro da imagem. (5) **Chromium:** `scripts/lib/buildPdf.mjs` ganhou `screenshotHtmlPng` (aditivo, viewport exato + clip, `document.fonts.ready`, guarda fail-closed de 8 MB) sem tocar no caminho A4. (6) **Privacidade/determinismo:** render 100% local; entrada, `chart-spec.json` e PNG gitignored (`data/graficos-instagram/`, `docs/research/graficos-instagram/`); sem schema/migration/DB. Crítica de fechamento do `designer` certificada (`Design tier: openai/gpt-5.6-sol`), sem bloqueantes; ajustes absorvidos (âncora sem repetir o número na manchete; stories com ≤5 pontos). Testes: `chartData`/`chartPrimitives`(golden)/`graficosInstagramRender`/prosa da skill + pinos de CI (`SCRIPTS_SPEC_PINNED`). `pnpm gate:fast` verde; prettier/typecheck/lint/knip limpos. Débito de runtime (entry/PNG real e parse ambíguo pt-BR) registrado em `C191-runtime` (#1151). diff --git a/docs/changelog/2026-09-18-c192.md b/docs/changelog/2026-09-18-c192.md new file mode 100644 index 00000000..7d25d36c --- /dev/null +++ b/docs/changelog/2026-09-18-c192.md @@ -0,0 +1 @@ +**C192 — Busca semântica no acervo de falas (2026-09-18):** o acervo (`/campanha/comunicacao/acervo`) ganhou busca **por tema** ao lado da busca literal, sem índice vetorial nem backfill. (1) **Modo explícito e visível**: novo parâmetro de URL `mode=tema` (default `termo`, omitido — deep links da busca exata ficam byte-idênticos; `mode=tema` sem `q` canonicaliza fora), registrado em `speechListParamNames`; o seletor "Buscar por: Termo exato | Por tema" (segmented control, `aria-pressed`) é otimista e preserva facetas/página. (2) **Ponte semântica determinística**: `expandSpeechSearchTheme` (`src/utilities/ai/`, `server-only`) pede ao DeepSeek (`generateObject`, `deepseek-flash`, timeout 4s, temperatura 0.2) de 3 a 8 termos relacionados; nunca lança — sem chave, timeout, erro ou saída malformada resolve `null` (degradação), e lista vazia é resposta legítima (sem aviso). `normalizeSpeechThemeTerms` sanitiza a saída do modelo (`%`/`_`/`\`, termos <3 chars, dedupe, teto de 8/60 chars). (3) **Motor lexical no caminho do Payload**: `buildSpeechListWhereIncludingCutOrigins` OR-a `q` + termos expandidos (`searchText like` / `keywords contains`) com dedupe, mantém facetas AND, paginação por fala e o OR de origem de corte do C174 (ancorado só no `q` literal); `payload.find` segue com `user` + `overrideAccess:false` e `canReadSpeechCatalog` barra a chamada externa de ator negado (fail-closed). (4) **Proveniência honesta**: `toSpeechListItemViewModel` ganha `themeMatchTerm` e `matchKind='theme'`; o card de tema mostra selo "Tema" (+ "Termo exato" quando também casa literal) e o bloco "Por que apareceu" com o excerto real da fala e a frase marcada como faixa contínua (`findPhraseRanges`), sem score nem texto gerado; o CTA só oferece "Assistir no trecho" com casamento literal real. (5) **Degradação e vazio**: aviso discreto (`Alert pending`, literais da cena 04) com "Tentar por tema novamente" e resultados lexicais; o vazio honesto tem as saídas reformular/usar termo exato/limpar filtros (cena 03). (6) **Design**: o `designer` estendeu o artefato do gate (`docs/plans/acervo-busca-semantica-ui-design.html`, cenas 01/02) e certificou a implementação renderizada (desktop 1280 / mobile 390). (7) **Testes**: novos unit de `speechThemeTerms` e `expandSpeechSearchTheme` (mock de provider) e extensões de URL/where/espelho/view model/omnibox; int com expansor injetado (tema, fallback, guard de acesso); e2e do modo degradado sem chave (o `playwright.config.ts` agora zera `DEEPSEEK_API_KEY` no servidor e2e). Sem migration, sem collection/Consent novo, sem exposição pública. `pnpm gate:ci` verde (unit 3742, int 827, e2e do acervo 16). diff --git a/docs/changelog/2026-09-18-c193-fix-rename.md b/docs/changelog/2026-09-18-c193-fix-rename.md new file mode 100644 index 00000000..dd8916cc --- /dev/null +++ b/docs/changelog/2026-09-18-c193-fix-rename.md @@ -0,0 +1 @@ +**C193 (fix) — restaura `main` após rename concorrente com o C192 (2026-09-18):** o rebase do merge do C193 renomeou `canReadSpeechCatalog` → `canReadCommunicationCatalog`, mas o C192 (mergeado minutos antes) adicionou novas referências ao nome antigo em `src/utilities/speech/speechPageData.ts` (import + gate do modo tema). O merge rebase juntou as duas mudanças sem conflito textual e o `verify` do deploy pegou a quebra (`TS2305`); a branch do PR estava baseada antes do C192, então o CI do PR não via o arquivo. Fix mecânico: as 2 referências passam ao nome novo. `pnpm gate:fast` verde (364 arquivos / 3867 testes) + int `speechAcervo`/`reel` 22 verdes. diff --git a/docs/changelog/2026-09-18-c193.md b/docs/changelog/2026-09-18-c193.md new file mode 100644 index 00000000..7d226fae --- /dev/null +++ b/docs/changelog/2026-09-18-c193.md @@ -0,0 +1 @@ +**C193 — Reel privado no CMS: modelo, arquivos privados e kill switch (2026-09-18):** a assessoria produz os Reels de tutorial fora do Teqo e eles não tinham onde existir. Entregou-se a base no `/campanha`: (1) **collection `reel`** (grupo `Comunicação`) com `title`, `feature` (lista fechada, começando `cards`), `status` `draft→published→unpublished`, e os artefatos como uploads 1:1 — `video` (MP4 sem áudio, primário, required), `videoWithAudio`, `narrationAudio`, `captions` (`.srt`), `cover` (required) — mais `transcript` em texto, `publishedAt` carimbado na transição para `published` e `createdBy` do ator; (2) **collection de upload privada `reelMedia`** separada de `media` (que é leitura pública por contrato, pinada no lockdown): `read/create/update/delete` só para `communicator`/`coordinator`/`candidate` + admin, registrada no plugin S3 (`reelMedia: true`) para não cair em disco efêmero em produção; (3) **rota autenticada de mídia** `GET /campanha/comunicacao/reels/[id]/media/[kind]` — sob `/campanha` de propósito, porque é o path do cookie `campaign-token` que `