mirror of
https://github.com/fsolla/teqo.git
synced 2026-11-12 22:13:05 +00:00
feat(C214): briefing 100% recorte — 4 folhas do recorte + âncora item-level (Closes #1284)
Replaneja o Briefing de capacitação C210: saem capa/objetivo/trajetória e as recomendações genéricas (roteiro de 5 passos, justificativa científica, evitar, conferir, limites); as 4 folhas passam a ser todas do recorte — defesas (princípios + posições com fonte), essencial (fatos-âncora com fase + pedido literal e uma linha de plano), qa 1/2 e qa 2/2. - briefingContent: contrato novo (plan no lugar de script.steps; avoid/checklist removidos; shed qa → defesas) e âncora item-level por sourceUrl — o id de checklist repete e o ledger resolvia pelo primeiro item; par errado falha fechado (normalizeQa não repassava o sourceUrl, corrigido). - briefingRender: 4 folhas novas (defesas/essencial/qa/qa-2) e companion .md; CSS portado do design (principles-copy/request-block/request-plan). - build-dossie-solla-briefing: âncoras e comentários do fluxo novo. - Design hi-fi refeito (4 cenas do recorte), skill e agente autor atualizados. - Specs briefingContent/briefingRender reescritos; gate:fast verde (lint, typecheck, 389 arquivos / 4310 testes).
This commit is contained in:
parent
2a0dca6243
commit
c6620c2511
10 changed files with 685 additions and 800 deletions
|
|
@ -1,15 +1,22 @@
|
|||
---
|
||||
name: briefing-capacitacao-solla
|
||||
description: 'Gera o Briefing de capacitação Solla 1313 por recorte (cidade · instituição · tema): PDF A4 de até 4 páginas + companion .md, derivado do dossiê já pesquisado (sem segunda pesquisa factual), com essencial com fonte, roteiro do pedido de voto (vote 1313 + plano de voto + compromisso nomeado), perguntas prováveis × melhores respostas dos dois lados e o que evitar; aceita um recorte ou um lote separado por vírgula.'
|
||||
description: 'Gera o Briefing de capacitação Solla 1313 por recorte (cidade · instituição · tema): PDF A4 de até 4 páginas + companion .md, derivado do dossiê já pesquisado (sem segunda pesquisa factual), todo de recorte — princípios/crenças e defesas de Solla com fonte, fatos-âncora e o pedido literal do voto 1313, e perguntas prováveis × melhores respostas dos dois lados; aceita um recorte ou um lote separado por vírgula.'
|
||||
---
|
||||
|
||||
# Briefing de capacitação Solla 1313 (C210)
|
||||
|
||||
Entrega, por recorte (**cidade · instituição · tema**), um **PDF A4 de até 4
|
||||
páginas + companion `.md`** para quem vai **pedir o voto 1313** no recorte:
|
||||
essencial com fato-âncora e fonte, o que Solla defende, **roteiro do pedido**
|
||||
("vote 1313" + plano de voto + compromisso nomeado), **perguntas prováveis ×
|
||||
melhores respostas** (ataques da direita **e** da esquerda) e **o que evitar**.
|
||||
páginas + companion `.md`** para quem vai **pedir o voto 1313** no recorte.
|
||||
Depois do replanejamento (2026-09-23), o briefing é **todo do recorte** — não tem
|
||||
capa, manual genérico, roteiro de passos, anti-padrões nem limites:
|
||||
|
||||
1. **O que Solla defende** — princípios/crenças do recorte (abertura) + posições
|
||||
com lastro (fonte e data);
|
||||
2. **O essencial do recorte** — fatos-âncora com **fase junto ao valor** e o
|
||||
**pedido literal** do voto + **uma linha** de plano;
|
||||
3. **Perguntas prováveis × melhores respostas (1/2 e 2/2)** — as perguntas reais
|
||||
do recorte, metade do documento, com os dois lados.
|
||||
|
||||
É **insumo interno de capacitação**: rótulo literal
|
||||
`Insumo interno de capacitação — não publicar` em todas as folhas, sem CTA
|
||||
público, sem marca publicável, artefato gitignored.
|
||||
|
|
@ -77,7 +84,8 @@ recorte **não** cancela os demais (**sucesso parcial** explícito).
|
|||
build/ssh, não commita.
|
||||
3. **Auditoria de citações (orquestrador).** Confere citação por citação (nome,
|
||||
data, valor, órgão, alcance) contra os `research.json`; todo item ancorado
|
||||
aponta um `factId` real e todo texto sem lastro é corrigido ou removido.
|
||||
aponta um `factId` real que **lastreia a nota** (o ledger resolve o id pelo
|
||||
primeiro item daquele id) e todo texto sem lastro é corrigido ou removido.
|
||||
Fato novo não entra: vira `gapReason`.
|
||||
4. **Build local (por recorte), offline:**
|
||||
```bash
|
||||
|
|
@ -90,7 +98,8 @@ recorte **não** cancela os demais (**sucesso parcial** explícito).
|
|||
`--unit=institution|theme` para os outros recortes (default `municipality`).
|
||||
O build valida o `briefing.json` **fechado** (âncora que não resolve em fato
|
||||
com fonte = erro; chave de cenário/staff-only = erro), renderiza as **4 folhas
|
||||
fixas**, mede cada folha e emite **1 PDF** com a guarda de teto de páginas
|
||||
fixas do recorte** (`defesas → essencial + pedido → qa 1/2 → qa 2/2`), mede
|
||||
cada folha e emite **1 PDF** com a guarda de teto de páginas
|
||||
(`emitHtmlSinglePdf`, `maxPages=4`). Saídas:
|
||||
`docs/research/dossie-solla-<recorte>/<slug>-<YYYY-MM-DD>-briefing.pdf` +
|
||||
`-briefing.md`; intermediários gitignored em `data/dossie-solla-<recorte>/`.
|
||||
|
|
@ -98,11 +107,11 @@ recorte **não** cancela os demais (**sucesso parcial** explícito).
|
|||
(ok|failed) · briefing (quando ok) · motivo (quando falha)`.
|
||||
|
||||
O teto de **4 páginas é rígido**: se uma folha estourar, o build **corta por
|
||||
prioridade declarada** (`qa` até o mínimo de 4 com os dois lados → `defesas` →
|
||||
`conferir` → `evitar` até o mínimo de 3) e **declara o resto na folha** ("e mais
|
||||
N no briefing completo (.md)"); o companion `.md` carrega **tudo**. Essencial,
|
||||
roteiro e identificação **nunca** são cortados; se nem o mínimo couber, o build
|
||||
falha fechado apontando a lista a encurtar.
|
||||
prioridade** (`qa` até o mínimo de 4 com os dois lados → `defesas` até esvaziar)
|
||||
e **declara o resto na folha** ("e mais N no briefing completo (.md)"); o
|
||||
companion `.md` carrega **tudo**. O essencial (fatos-âncora), o **pedido** e a
|
||||
identificação **nunca** são cortados; se nem o mínimo couber, o build falha
|
||||
fechado apontando a lista a encurtar.
|
||||
|
||||
## Contrato do `<slug>.briefing.json`
|
||||
|
||||
|
|
@ -112,44 +121,48 @@ Escrito pelo autor, validado pelo build (`scripts/lib/briefingContent.mjs`):
|
|||
{
|
||||
"unitId": "municipality", // municipality | institution | theme
|
||||
"municipalitySlug": "miguel-calmon", // chave = slugField da unidade (institutionSlug | themeSlug)
|
||||
"generatedAt": "2026-09-22T12:00:00.000Z", // obrigatório
|
||||
"generatedAt": "2026-09-23T12:00:00.000Z", // obrigatório
|
||||
"subtitle": "Piemonte da Diamantina · consulta antes e durante o contato", // opcional
|
||||
"lede": "≤380 chars — como usar o essencial",
|
||||
"essential": [ // ≥3; cada item: factId XOR gapReason
|
||||
{ "factId": "era_b_equipamentos", "title": "≤120", "note": "≤200" },
|
||||
"lede": "≤380 chars — princípios e crenças do recorte (abre a folha 1)",
|
||||
"defenses": [ // posições com lastro (folha 1); [] = linha de lacuna
|
||||
{ "factId": "era_b_defesas", "sourceUrl": "https://…", "title": "≤120", "note": "≤200" }
|
||||
],
|
||||
"essential": [ // ≥3; fatos-âncora (folha 2); cada item: factId XOR gapReason
|
||||
{ "factId": "era_b_equipamentos", "sourceUrl": "https://…", "title": "≤120", "note": "≤200" },
|
||||
{ "gapReason": "sem fala própria localizada", "title": "…", "note": "…" }
|
||||
],
|
||||
"defenses": [ { "factId": "…", "title": "…", "note": "…" } ], // opcional (C209); [] = linha de lacuna
|
||||
"script": { "steps": [ { "title": "≤90", "note": "≤220" } ] }, // ≥3 passos; o pedido é literal do renderer
|
||||
"plan": "≤220 chars — uma linha de plano de voto/compromisso nomeado", // obrigatório
|
||||
"qa": [ // ≥4, com ≥1 "direita" e ≥1 "esquerda"
|
||||
{ "side": "direita|esquerda|entrega", "question": "≤180",
|
||||
"acknowledge": "≤200", "answer": "≤520", "close": "≤200",
|
||||
"factId": "era_b_sesab" /* XOR */ "gapReason": "sem registro localizado" }
|
||||
],
|
||||
"avoid": [ { "title": "≤120", "note": "≤200" } ], // ≥3
|
||||
"checklist": { "beforeAnswer": ["≤200"], "unsure": ["≤200"] } // ≥2 em cada; **negrito** no prefixo
|
||||
"factId": "era_b_sesab", "sourceUrl": "https://…" /* XOR */ "gapReason": "sem registro localizado" }
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
Regras duras:
|
||||
|
||||
- `unitId` e o slug têm de casar com o recorte do build (outro recorte = erro);
|
||||
- cada item de `essential`/`defenses`/`qa` exige **exatamente um** de
|
||||
- cada item de `defenses`/`essential`/`qa` exige **exatamente um** de
|
||||
`factId | gapReason`; `factId` tem de resolver num fato do ledger **com
|
||||
`sourceUrl`** — **sem fonte, o item não entra**;
|
||||
- o id de checklist **se repete** entre itens do research e o ledger resolve o
|
||||
`factId` puro pelo **primeiro item** daquele id; para ancorar outro item, o
|
||||
autor copia o `sourceUrl` do item do research no campo `sourceUrl` do âncora
|
||||
(o par `factId` + `sourceUrl` precisa bater — par errado falha fechado);
|
||||
- `qa` cobre os dois lados (≥1 `direita` e ≥1 `esquerda`); `entrega` é opcional;
|
||||
- **proibido** qualquer chave de cenário/estimativa/staff-only
|
||||
(`estimatedVotes`, `scenario`, `projection`, `polls`, …) — o briefing não tem
|
||||
campo numérico; valor/fase vêm do fato-âncora no render;
|
||||
- caps de texto acima do limite são **aviso** (o autor encurta); quem falha
|
||||
fechado é a guarda de fit/páginas;
|
||||
- no `checklist`, o autor marca em `**negrito**` o prefixo que a folha imprime
|
||||
como palavra-chave de varredura (`"**Fonte e data** do fato…"`) — é o único
|
||||
marcador inline aceito.
|
||||
- a folha 1 abre com `lede` (princípios/crenças) e a lista `defenses` — o autor
|
||||
deve trazer **6–10 defesas** para encher a folha; a folha 3–4 acomoda **8–10
|
||||
perguntas** (o build divide a lista ao meio).
|
||||
|
||||
O pedido é **literal do renderer** (não reescreva no JSON): "Posso contar com
|
||||
você? Para deputado federal, **vote 1313, Jorge Solla**." — com plano de voto
|
||||
(onde/quando/como) e compromisso nomeado nos passos do roteiro.
|
||||
você? Para deputado federal, **vote 1313, Jorge Solla**." — o `plan` é a única
|
||||
linha de orientação que o autor escreve.
|
||||
|
||||
## Recibo do autor
|
||||
|
||||
|
|
@ -162,9 +175,10 @@ JSON:
|
|||
"slug": "miguel-calmon",
|
||||
"status": "ok", // ou "failed"
|
||||
"briefingPath": "data/dossie-solla-cidade/miguel-calmon.briefing.json",
|
||||
"essentialCount": 4,
|
||||
"qaCount": 5,
|
||||
"sides": ["direita", "esquerda"],
|
||||
"essentialCount": 5,
|
||||
"defensesCount": 8,
|
||||
"qaCount": 9,
|
||||
"sides": ["direita", "esquerda", "entrega"],
|
||||
"anchoredFactIds": ["era_b_equipamentos", "era_b_programas", "era_c_atuacao"],
|
||||
"gapCount": 3, // itens com gapReason + lacunas do recorte
|
||||
"failureReason": "…" // opcional (só quando status = failed)
|
||||
|
|
@ -175,24 +189,25 @@ JSON:
|
|||
no summary final. Arquivo ausente = o build falha com o ponteiro para esta
|
||||
skill (não há fallback determinístico: o briefing é redação, não consolidação).
|
||||
|
||||
## Conteúdo do briefing (4 folhas fixas)
|
||||
## Conteúdo do briefing (4 folhas fixas, todas do recorte)
|
||||
|
||||
- **Folha 1 — O essencial do recorte** (`data-page="essencial"`): identificação
|
||||
do recorte + lede ("como usar") + fatos-âncora com fonte ou lacuna declarada +
|
||||
regra do recorte (esfera/fase) no rodapé.
|
||||
- **Folha 2 — Defesas e pedido** (`data-page="defesas"`): "o que Solla defende"
|
||||
(C209 quando existir; sem registro = linha de lacuna, nunca proposta genérica)
|
||||
+ roteiro do pedido com o literal do voto 1313 + 3–5 passos (relação → fato →
|
||||
pedido explícito → plano de voto → compromisso nomeado).
|
||||
- **Folha 3 — Perguntas prováveis × melhores respostas** (`data-page="qa"`):
|
||||
régua visível (reconhecer → fato local verificável → fechar no pedido) + Q&A
|
||||
dos dois lados; resposta ancorada num fato-âncora ou declarada como lacuna ("o
|
||||
dossiê não sustenta; vou conferir").
|
||||
- **Folha 4 — O que evitar + conferência + limites** (`data-page="evitar"`):
|
||||
anti-padrões (confronto, humilhação, repetir o ataque, broadcast impessoal,
|
||||
prometer sem lastro), "o que conferir no dossiê", "se você não souber" e os
|
||||
limites/defeso (inclusive: evidência internacional orienta o método, não mede
|
||||
o Brasil).
|
||||
- **Folha 1 — O que Solla defende** (`data-page="defesas"`): princípios/crenças
|
||||
do recorte (o `lede`) e as posições (`defenses`) com título, leitura e
|
||||
`fonte · data`. Sem registro = linha de lacuna, nunca posição genérica.
|
||||
- **Folha 2 — O essencial do recorte + O pedido** (`data-page="essencial"`):
|
||||
fatos-âncora com **fase junto ao valor** e fonte (sem trajetória de formação) e
|
||||
o bloco do pedido com a frase literal + a linha de `plan`.
|
||||
- **Folha 3 — Perguntas prováveis × melhores respostas (1/2)** (`data-page="qa"`):
|
||||
régua curta (reconhecer → fato com fonte → fechar no pedido) e a primeira
|
||||
metade do Q&A (pergunta + reconhecer/fato/fechar), dois lados.
|
||||
- **Folha 4 — Perguntas prováveis × melhores respostas (2/2)**
|
||||
(`data-page="qa-2"`): continuação com a segunda metade do Q&A e o "e mais N"
|
||||
quando houver corte.
|
||||
|
||||
**Fora do briefing** (replanejamento 2026-09-23): capa/objetivo, trajetória,
|
||||
roteiro de passos, justificativa científica, "o que evitar", "o que conferir",
|
||||
"se você não souber" e "limites e defeso" — o rótulo interno em todas as folhas
|
||||
já carrega o aviso de insumo.
|
||||
|
||||
## Guardrails de produto (não negociáveis)
|
||||
|
||||
|
|
@ -223,7 +238,7 @@ skill (não há fallback determinístico: o briefing é redação, não consolid
|
|||
está sem fonte — troque por um `factId` real ou declare `gapReason`; nunca
|
||||
"aproxime" o texto.
|
||||
- **Folha estourou**: o build corta por prioridade e declara o resto; se a
|
||||
mensagem disser que nem o mínimo cabe, **encurte as listas** (QA/essencial),
|
||||
mensagem disser que nem o mínimo cabe, **encurte as listas** (Q&A/defesas),
|
||||
não o layout.
|
||||
- **`unitId`/slug de outro recorte**: o build recusa o par — pare e regenere o
|
||||
`briefing.json` para o recorte certo.
|
||||
|
|
@ -250,9 +265,3 @@ skill (não há fallback determinístico: o briefing é redação, não consolid
|
|||
`.agents/skills/dossie-solla-tema/SKILL.md`.
|
||||
- Voz/rebates: `.opencode/skills/solla-comunicacao/SKILL.md`,
|
||||
`.opencode/skills/solla-comunicacao/referencia/tom-e-exemplos.md`.
|
||||
- Evidência de mobilização citada no treinamento: Nickerson & Rogers 2010;
|
||||
Gerber & Green 2000; Kalla & Broockman 2018/2020; Schein et al. 2021;
|
||||
Michelson et al. 2024; Nickerson 2008; Gerber & Rogers 2009; Gerber, Green &
|
||||
Larimer 2008; Cialdini & Goldstein 2004; Broockman & Kalla 2016; Grimmer,
|
||||
Messing & Westwood 2012; Wood & Porter 2019; Lau, Sigelman & Rovner 2007;
|
||||
Ecker et al. 2022; Debunking Handbook 2020.
|
||||
|
|
|
|||
|
|
@ -18,11 +18,20 @@ dossiê daquele recorte estar pesquisado.
|
|||
`gaps[]` são lacunas explícitas) e, quando existir, o
|
||||
`<slug>.narrative.json`.
|
||||
2. Escrever `data/dossie-solla-<recorte>/<slug>.briefing.json` no contrato da
|
||||
skill: `unitId` + slug do recorte, `generatedAt`, `lede`, `essential`
|
||||
(≥3, cada item `factId` **ou** `gapReason`), `defenses` (opcional),
|
||||
`script.steps` (≥3), `qa` (≥4, com os dois lados), `avoid` (≥3) e
|
||||
`checklist.beforeAnswer`/`unsure` (≥2 cada; marque em `**negrito**` o
|
||||
prefixo de varredura, ex. `"**Fonte e data** do fato que pretende usar."`).
|
||||
skill, **todo de recorte** (o briefing não tem capa, roteiro, anti-padrões
|
||||
nem limites):
|
||||
- `lede` — princípios e crenças do recorte (≤380 chars, abre a folha 1);
|
||||
- `defenses` — **6–10** posições de Solla no recorte, cada uma com título,
|
||||
leitura e âncora (`factId` real de item com fonte, ou `gapReason`); sem
|
||||
registro = lacuna, nunca posição genérica;
|
||||
- `essential` (≥3) — fatos-âncora que sustentam a conversa (sem trajetória de
|
||||
formação), com a fase junto ao valor;
|
||||
- `plan` — **uma linha** (≤220 chars) de plano de voto/compromisso nomeado
|
||||
(o pedido literal é do renderer, não o reescreva);
|
||||
- `qa` (≥4; mire **8–10**) — perguntas prováveis com `side`
|
||||
(`direita|esquerda|entrega`), `question`, `acknowledge`, `answer` (ancorada
|
||||
em `factId` real ou declarada como lacuna) e `close`, cobrindo os dois
|
||||
lados.
|
||||
3. Devolver **apenas o recibo curto** (seção "Recibo do autor" da skill).
|
||||
|
||||
## Limites
|
||||
|
|
@ -30,6 +39,10 @@ dossiê daquele recorte estar pesquisado.
|
|||
- **Só fatos dos itens com fonte**: cada `factId` precisa ser o `id` de um item
|
||||
real do research daquele recorte; nunca invente id, número, data, órgão ou
|
||||
valor. O build **falha fechado** se o `factId` não resolver em fato com fonte.
|
||||
Lembre: o id de checklist **se repete** e o ledger resolve o `factId` puro pelo
|
||||
**primeiro item** daquele id — quando a nota se apoia em outro item, copie o
|
||||
`sourceUrl` do item do research no campo `sourceUrl` do âncora (o par
|
||||
`factId` + `sourceUrl` tem de bater; par errado falha fechado).
|
||||
- Sem lastro **não é resposta**: vira `gapReason` e o texto da resposta assume a
|
||||
lacuna ("não tenho esse dado confirmado aqui; vou conferir no dossiê").
|
||||
- **Nunca** use `região`/`setor`/`segmento`/`rede` como se fosse entrega
|
||||
|
|
@ -38,7 +51,7 @@ dossiê daquele recorte estar pesquisado.
|
|||
(`estimatedVotes`, `scenario`, `projection`, `polls`, …): o briefing é
|
||||
capacitação, não leitura eleitoral.
|
||||
- Copy pt-BR, sem PII (telefone/e-mail nunca), sem CTA público, sem promessa de
|
||||
entrega; o pedido literal do voto 1313 é do renderer — não o reescreva.
|
||||
entrega; o pedido literal do voto 1313 é do renderer.
|
||||
- Reuse a voz do `solla-comunicacao` (rebates e fórmula de resposta a crítica),
|
||||
sem inventar tom novo.
|
||||
- **Não** faça pesquisa web nova, **não** edite os `research.json`, **não** rode
|
||||
|
|
|
|||
1
docs/changelog/2026-09-23-c214.md
Normal file
1
docs/changelog/2026-09-23-c214.md
Normal file
|
|
@ -0,0 +1 @@
|
|||
**C214 — Briefing C210 replanejado: 4 folhas 100% recorte (2026-09-23):** o Briefing de capacitação da família de dossiês (cidade/instituição/tema) foi replanejado a partir da crítica de produto: as 4 folhas fixas deixaram de ter capa/objetivo/trajetória e recomendações genéricas (roteiro de 5 passos, justificativa científica, "o que evitar", "o que conferir", "se você não souber" e "limites/defeso") e passaram a ser **todas do recorte** — `defesas` (princípios/crenças + posições com fonte), `essencial` (fatos-âncora com fase + o pedido literal e uma linha de plano), `qa` e `qa-2` (perguntas prováveis, metade do documento, 1/2 e 2/2). O contrato do `briefing.json` mudou: `script.steps`/`avoid`/`checklist` saíram, entrou `plan` (1 linha), `defenses` virou a folha de abertura e o Q&A foi ampliado (8–10 no alvo); o shed determinístico agora é `qa → defesas`, com essencial/pedido nunca cortados. O dono `scripts/lib/briefingContent.mjs` ganhou **âncora item-level**: como o id de checklist se repete entre itens e o ledger resolvia pelo primeiro, o âncora passou a aceitar `sourceUrl` (copiado do item do research) para resolver o par exato `factId` + `sourceUrl` — par errado falha fechado (o Q&A não repassava o `sourceUrl`, corrigido junto). O design hi-fi `docs/plans/briefing-capacitacao-solla-ui-design.html` foi refeito pelo designer (4 cenas novas, classes `.principles-copy`/`.request-block`/`.request-plan`; removidas `.lede`/`.script-list`/`.avoid-list`/`.check-list`/`.two-column`/`.limits`/`.sheet-compact`), a skill e o agente autor foram atualizados e os specs `briefingContent`/`briefingRender`/`briefingSkill` reescritos. O briefing da UFBA foi re-autorado (10 defesas, 8 fatos-âncora, 12 perguntas, âncoras item-level auditadas) e sai em 4 páginas sem cortes. Sem `src/`, sem schema/migration/rota; `pnpm gate:fast` verde (lint, typecheck, 389 arquivos / 4310 testes). Issue: #1284.
|
||||
|
|
@ -1,5 +1,7 @@
|
|||
# Impl: Briefing de capacitação Solla 1313 — até 4 páginas para quem vai pedir voto no recorte (cidade · instituição · tema)
|
||||
|
||||
> **Replanejado em 2026-09-23** (4 folhas 100% recorte; âncora item-level por `sourceUrl`; `plan` no lugar do roteiro; sem evitar/conferir/limites): ver `docs/changelog/2026-09-23-c210-replan.md`, a skill atualizada e o design `docs/plans/briefing-capacitacao-solla-ui-design.html`. Este plano fica como histórico do desenho original (C210).
|
||||
|
||||
Status: aprovado
|
||||
Atualizado em: 2026-09-22
|
||||
Issue: #1252
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4"></script>
|
||||
<title>Design — Briefing de capacitação Solla</title>
|
||||
<title>Design — Briefing de capacitação Solla · defesas, essencial e Q&A</title>
|
||||
<style type="text/tailwindcss">
|
||||
@theme {
|
||||
--color-ink: #18232d;
|
||||
|
|
@ -156,8 +156,8 @@
|
|||
letter-spacing: -0.015em;
|
||||
}
|
||||
|
||||
.lede {
|
||||
max-width: 158mm;
|
||||
.principles-copy {
|
||||
max-width: 162mm;
|
||||
margin: 3mm 0 0;
|
||||
font-size: 10.2pt;
|
||||
line-height: 1.5;
|
||||
|
|
@ -178,31 +178,22 @@
|
|||
color: var(--ink);
|
||||
}
|
||||
|
||||
.line-list,
|
||||
.script-list,
|
||||
.avoid-list,
|
||||
.check-list {
|
||||
.line-list {
|
||||
margin: 3mm 0 0;
|
||||
padding: 0;
|
||||
border-top: 0.35mm solid var(--ink);
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.line-list li,
|
||||
.script-list li,
|
||||
.avoid-list li,
|
||||
.check-list li {
|
||||
.line-list li {
|
||||
position: relative;
|
||||
padding: 2.5mm 0 2.5mm 6mm;
|
||||
padding: 2.35mm 0 2.35mm 6mm;
|
||||
border-bottom: 0.2mm solid var(--line-light);
|
||||
}
|
||||
|
||||
.line-list li::before,
|
||||
.script-list li::before,
|
||||
.avoid-list li::before,
|
||||
.check-list li::before {
|
||||
.line-list li::before {
|
||||
position: absolute;
|
||||
top: 3.7mm;
|
||||
top: 3.55mm;
|
||||
left: 0;
|
||||
width: 2.2mm;
|
||||
height: 0.45mm;
|
||||
|
|
@ -217,10 +208,10 @@
|
|||
|
||||
.line-meta {
|
||||
display: block;
|
||||
margin-top: 0.8mm;
|
||||
margin-top: 0.7mm;
|
||||
color: var(--ink-soft);
|
||||
font-size: 7.8pt;
|
||||
line-height: 1.4;
|
||||
line-height: 1.36;
|
||||
}
|
||||
|
||||
.source {
|
||||
|
|
@ -229,11 +220,24 @@
|
|||
font-weight: 700;
|
||||
}
|
||||
|
||||
.more-note {
|
||||
margin: 3mm 0 0;
|
||||
color: var(--ink-soft);
|
||||
font-size: 8pt;
|
||||
font-weight: 600;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.request-block {
|
||||
margin-top: 5mm;
|
||||
padding: 4.5mm 5mm 4mm;
|
||||
border: 0.35mm solid var(--line);
|
||||
border-left: 1.2mm solid var(--accent);
|
||||
background: var(--note);
|
||||
}
|
||||
|
||||
.request-line {
|
||||
margin: 3.5mm 0 0;
|
||||
padding: 4mm 0;
|
||||
border-top: 0.6mm solid var(--ink);
|
||||
border-bottom: 0.6mm solid var(--ink);
|
||||
margin: 2mm 0 0;
|
||||
font-size: 14pt;
|
||||
font-weight: 650;
|
||||
line-height: 1.3;
|
||||
|
|
@ -245,25 +249,17 @@
|
|||
font-style: normal;
|
||||
}
|
||||
|
||||
.script-list {
|
||||
counter-reset: script-step;
|
||||
.request-plan {
|
||||
margin: 3mm 0 0;
|
||||
padding-top: 2.5mm;
|
||||
border-top: 0.25mm solid var(--line);
|
||||
color: var(--ink-soft);
|
||||
font-size: 8.6pt;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.script-list li {
|
||||
min-height: 14mm;
|
||||
padding-left: 13mm;
|
||||
counter-increment: script-step;
|
||||
}
|
||||
|
||||
.script-list li::before {
|
||||
top: 2.7mm;
|
||||
width: 8mm;
|
||||
height: auto;
|
||||
color: var(--accent);
|
||||
background: none;
|
||||
content: counter(script-step, decimal-leading-zero);
|
||||
font-size: 8pt;
|
||||
font-weight: 700;
|
||||
.request-plan strong {
|
||||
color: var(--ink);
|
||||
}
|
||||
|
||||
.qa-list {
|
||||
|
|
@ -307,31 +303,6 @@
|
|||
font-weight: 700;
|
||||
}
|
||||
|
||||
.avoid-list strong,
|
||||
.check-list strong {
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.two-column {
|
||||
display: grid;
|
||||
margin-top: 5mm;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 8mm;
|
||||
}
|
||||
|
||||
.limits {
|
||||
margin-top: 5mm;
|
||||
padding-top: 3mm;
|
||||
border-top: 0.6mm solid var(--ink);
|
||||
}
|
||||
|
||||
.limits p {
|
||||
margin: 1.5mm 0 0;
|
||||
color: var(--ink-soft);
|
||||
font-size: 8pt;
|
||||
line-height: 1.42;
|
||||
}
|
||||
|
||||
.brief-footer {
|
||||
display: flex;
|
||||
margin-top: auto;
|
||||
|
|
@ -348,52 +319,6 @@
|
|||
color: var(--ink);
|
||||
}
|
||||
|
||||
.sheet-compact .section {
|
||||
margin-top: 3.8mm;
|
||||
}
|
||||
|
||||
.sheet-compact .avoid-list,
|
||||
.sheet-compact .check-list {
|
||||
margin-top: 2mm;
|
||||
}
|
||||
|
||||
.sheet-compact .avoid-list li,
|
||||
.sheet-compact .check-list li {
|
||||
padding-top: 1.65mm;
|
||||
padding-bottom: 1.65mm;
|
||||
font-size: 8.7pt;
|
||||
line-height: 1.32;
|
||||
}
|
||||
|
||||
.sheet-compact .avoid-list li::before,
|
||||
.sheet-compact .check-list li::before {
|
||||
top: 2.8mm;
|
||||
}
|
||||
|
||||
.sheet-compact .two-column {
|
||||
margin-top: 3.8mm;
|
||||
gap: 6mm;
|
||||
}
|
||||
|
||||
.sheet-compact .section-title {
|
||||
font-size: 12.5pt;
|
||||
}
|
||||
|
||||
.sheet-compact .limits {
|
||||
margin-top: 3.5mm;
|
||||
padding-top: 2mm;
|
||||
}
|
||||
|
||||
.sheet-compact .limits p {
|
||||
margin-top: 1mm;
|
||||
font-size: 7.5pt;
|
||||
line-height: 1.32;
|
||||
}
|
||||
|
||||
.sheet-compact .brief-footer {
|
||||
padding-top: 1.8mm;
|
||||
}
|
||||
|
||||
@media (max-width: 760px) {
|
||||
.gate-intro {
|
||||
width: calc(100% - 20px);
|
||||
|
|
@ -426,7 +351,7 @@
|
|||
}
|
||||
|
||||
.brief-subtitle,
|
||||
.lede {
|
||||
.principles-copy {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
|
|
@ -451,33 +376,31 @@
|
|||
font-size: 20px;
|
||||
}
|
||||
|
||||
.line-list li,
|
||||
.script-list li,
|
||||
.avoid-list li,
|
||||
.check-list li {
|
||||
.line-list li {
|
||||
padding-top: 12px;
|
||||
padding-bottom: 12px;
|
||||
}
|
||||
|
||||
.line-list li::before,
|
||||
.avoid-list li::before,
|
||||
.check-list li::before {
|
||||
.line-list li::before {
|
||||
top: 20px;
|
||||
}
|
||||
|
||||
.line-meta,
|
||||
.qa-question,
|
||||
.qa-answer,
|
||||
.limits p {
|
||||
.request-plan {
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.qa-item,
|
||||
.two-column {
|
||||
.qa-item {
|
||||
grid-template-columns: 1fr;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.request-block {
|
||||
padding: 18px;
|
||||
}
|
||||
|
||||
.request-line {
|
||||
font-size: 19px;
|
||||
}
|
||||
|
|
@ -520,396 +443,423 @@
|
|||
<body>
|
||||
<header class="gate-intro">
|
||||
<p class="m-0 text-xs font-bold uppercase tracking-[.14em] text-accent">
|
||||
C210 · referência visual do gate
|
||||
C210 · referência visual replanejada
|
||||
</p>
|
||||
<h1 class="mt-2 text-2xl font-semibold tracking-tight text-ink">
|
||||
Briefing de capacitação · família sóbria
|
||||
Briefing de capacitação · quatro folhas do recorte
|
||||
</h1>
|
||||
<p class="mt-2 max-w-3xl text-sm leading-6 text-ink-soft">
|
||||
Quatro folhas A4, sem marca de campanha, cartões, selos ou gráficos. No celular, as mesmas
|
||||
folhas refluem em uma coluna com corpo de 14 px para consulta em campo. O conteúdo é insumo
|
||||
interno: fatos vêm do dossiê; lacunas permanecem lacunas.
|
||||
O briefing deixa de funcionar como capa ou manual genérico: abre pelas posições de Solla,
|
||||
concentra os fatos que sustentam a conversa e dedica metade do documento às perguntas reais
|
||||
do recorte. A referência abaixo usa a UFBA para provar densidade e fit; o port mantém a
|
||||
mesma gramática sóbria da família, sem marca de campanha.
|
||||
</p>
|
||||
</header>
|
||||
|
||||
<main>
|
||||
<section class="scene">
|
||||
<p class="scene-label">Folha 1/4 · O essencial do recorte</p>
|
||||
<article class="sheet" aria-label="O essencial do briefing de Miguel Calmon">
|
||||
<p class="scene-label">Folha 1/4 · O que Solla defende</p>
|
||||
<article class="sheet" data-page="defesas" aria-label="O que Solla defende na UFBA">
|
||||
<header class="brief-head">
|
||||
<div>
|
||||
<p class="kicker">Briefing de capacitação · cidade</p>
|
||||
<h2 class="brief-title">Miguel Calmon</h2>
|
||||
<p class="brief-subtitle">
|
||||
Piemonte da Diamantina · consulta antes e durante o contato
|
||||
</p>
|
||||
<p class="kicker">Briefing de capacitação · instituição</p>
|
||||
<h2 class="brief-title">O que Solla defende</h2>
|
||||
<p class="brief-subtitle">UFBA · princípios que orientam posição e voto</p>
|
||||
</div>
|
||||
<p class="internal-label">
|
||||
Insumo interno de capacitação — não publicar
|
||||
INSUMO INTERNO DE CAPACITAÇÃO — NÃO PUBLICAR
|
||||
<span class="page-no">folha 1/4</span>
|
||||
</p>
|
||||
</header>
|
||||
|
||||
<section class="section">
|
||||
<p class="eyebrow">Comece por aqui</p>
|
||||
<h3 class="section-title">O essencial do recorte</h3>
|
||||
<p class="lede">
|
||||
Use um fato local de cada vez. Separe o que ocorreu no município do que atende a
|
||||
região. Não transforme articulação, indicação ou valor autorizado em entrega
|
||||
concluída.
|
||||
<p class="eyebrow">Princípios e crenças · leitura dos registros, não opinião</p>
|
||||
<p class="principles-copy">
|
||||
Universidade pública forte combina autonomia, acesso e permanência. Para Solla, o
|
||||
conhecimento só cumpre seu papel quando chega ao povo — no ensino, na pesquisa e no
|
||||
SUS — com financiamento público e respeito à comunidade universitária.
|
||||
</p>
|
||||
<ul class="line-list">
|
||||
<li>
|
||||
<span class="line-title"
|
||||
>A evidência municipal mais concentrada está na gestão estadual da saúde.</span
|
||||
>Hospitais universitários devem permanecer integrados ao SUS.</span
|
||||
>
|
||||
<span class="line-meta"
|
||||
>Ambulância, Saúde em Movimento, Olhar Brasil, atenção domiciliar e gestão da
|
||||
Santa Casa aparecem com menção direta a Miguel Calmon.
|
||||
<span class="source">[B-01–B-07]</span></span
|
||||
>Ensino, pesquisa e assistência não são missões concorrentes: o hospital forma
|
||||
profissionais enquanto atende quem precisa.
|
||||
<span class="source">Hupes no SUS · 2007</span></span
|
||||
>
|
||||
</li>
|
||||
<li>
|
||||
<span class="line-title"
|
||||
>A Santa Casa aparece com 80 leitos e emergência 24 horas no registro de
|
||||
2012.</span
|
||||
>Autonomia universitária inclui respeitar a escolha de reitor.</span
|
||||
>
|
||||
<span class="line-meta"
|
||||
>Fase do valor anual não informada: cite o serviço, não some valores.
|
||||
<span class="source">Jornal Grande Bahia · 25/01/2012</span></span
|
||||
>A comunidade deve participar da direção da universidade; lista tríplice não pode
|
||||
servir para ignorar o resultado da consulta.
|
||||
<span class="source">Câmara dos Deputados · 2019</span></span
|
||||
>
|
||||
</li>
|
||||
<li>
|
||||
<span class="line-title"
|
||||
>No mandato federal, os registros localizados têm alcance regional.</span
|
||||
>Educação e ciência precisam ficar fora do aperto fiscal.</span
|
||||
>
|
||||
<span class="line-meta"
|
||||
>Campus da UFBA, Policlínica, seminário e articulações em Jacobina não são
|
||||
entregas exclusivas ao município. <span class="source">[C-01–C-08]</span></span
|
||||
>Custeio e investimento previsíveis protegem laboratórios, aulas, hospitais e a
|
||||
capacidade de planejar.
|
||||
<span class="source">Registros legislativos · 2016–2023</span></span
|
||||
>
|
||||
</li>
|
||||
<li>
|
||||
<span class="line-title"
|
||||
>Não há fala própria localizada com menção nominal à cidade.</span
|
||||
>Expandir é levar universidade pública para além da capital.</span
|
||||
>
|
||||
<span class="line-meta"
|
||||
>Resultado de busca não prova ausência histórica. Se perguntarem além do que está
|
||||
documentado, anote e confira no dossiê. <span class="source">[L-07]</span></span
|
||||
>Interiorização aproxima formação, pesquisa e desenvolvimento das necessidades de
|
||||
cada território baiano.
|
||||
<span class="source">Atuação parlamentar · 2023</span></span
|
||||
>
|
||||
</li>
|
||||
<li>
|
||||
<span class="line-title"
|
||||
>Entrar não basta: permanência estudantil sustenta o acesso.</span
|
||||
>
|
||||
<span class="line-meta"
|
||||
>Assistência, bolsas e condições de estudo fazem a política de inclusão chegar à
|
||||
formatura. <span class="source">Registros sobre educação · 2019–2024</span></span
|
||||
>
|
||||
</li>
|
||||
<li>
|
||||
<span class="line-title">Cotas corrigem barreiras históricas de acesso.</span>
|
||||
<span class="line-meta"
|
||||
>A universidade pública se fortalece quando representa melhor a sociedade que a
|
||||
financia e serve.
|
||||
<span class="source">Defesa da universidade pública · 2019</span></span
|
||||
>
|
||||
</li>
|
||||
<li>
|
||||
<span class="line-title"
|
||||
>Patrimônio universitário precisa voltar ao uso acadêmico.</span
|
||||
>
|
||||
<span class="line-meta"
|
||||
>Restaurar a Famed preserva memória, mas também recupera espaço para ensino,
|
||||
pesquisa e extensão.
|
||||
<span class="source">Famed/UFBA · registros 2015–2026</span></span
|
||||
>
|
||||
</li>
|
||||
<li>
|
||||
<span class="line-title"
|
||||
>Defender a UFBA é defender um serviço público essencial.</span
|
||||
>
|
||||
<span class="line-meta"
|
||||
>Em 2019, a resposta aos cortes ligou universidade, soberania científica e
|
||||
direitos sociais — sem tratar educação como privilégio.
|
||||
<span class="source">Mobilização nacional · 2019</span></span
|
||||
>
|
||||
</li>
|
||||
</ul>
|
||||
<p class="more-note">
|
||||
Se o orçamento de impressão estourar: exibir “e mais N defesas no briefing completo
|
||||
(.md)”.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<div class="rule-note">
|
||||
<strong>Como usar.</strong> Antes: escolha dois fatos e ensaie o pedido. Durante:
|
||||
escute, responda com um fato verificável e peça o voto. Depois: registre a dúvida que
|
||||
ficou sem resposta para o próximo ciclo do dossiê.
|
||||
</div>
|
||||
|
||||
<footer class="brief-footer">
|
||||
<span
|
||||
><strong>Regra:</strong> município ≠ região · fase acompanha valor · sem fonte, não
|
||||
afirmar</span
|
||||
>
|
||||
<span>Briefing de capacitação · 1/4</span>
|
||||
<span><strong>Recorte:</strong> instituição · UFBA</span>
|
||||
<span>folha 1/4</span>
|
||||
</footer>
|
||||
</article>
|
||||
</section>
|
||||
|
||||
<section class="scene">
|
||||
<p class="scene-label">Folha 2/4 · Defesas + roteiro do pedido</p>
|
||||
<article class="sheet" aria-label="O que Solla defende e roteiro do pedido de voto">
|
||||
<p class="scene-label">Folha 2/4 · O essencial do recorte + O pedido</p>
|
||||
<article
|
||||
class="sheet"
|
||||
data-page="essencial"
|
||||
aria-label="O essencial da UFBA e o pedido de voto"
|
||||
>
|
||||
<header class="brief-head">
|
||||
<div>
|
||||
<p class="kicker">Briefing de capacitação · Miguel Calmon</p>
|
||||
<h2 class="brief-title">Defesas e pedido de voto</h2>
|
||||
<p class="brief-subtitle">Do fato local ao compromisso nomeado</p>
|
||||
<p class="kicker">Briefing de capacitação · instituição</p>
|
||||
<h2 class="brief-title">O essencial do recorte</h2>
|
||||
<p class="brief-subtitle">UFBA · fatos que sustentam a conversa</p>
|
||||
</div>
|
||||
<p class="internal-label">
|
||||
Insumo interno de capacitação — não publicar
|
||||
INSUMO INTERNO DE CAPACITAÇÃO — NÃO PUBLICAR
|
||||
<span class="page-no">folha 2/4</span>
|
||||
</p>
|
||||
</header>
|
||||
|
||||
<section class="section">
|
||||
<p class="eyebrow">Leitura dos registros · não é opinião</p>
|
||||
<h3 class="section-title">O que Solla defende</h3>
|
||||
<p class="eyebrow">Fatos-âncora · fase junto ao valor</p>
|
||||
<ul class="line-list">
|
||||
<li>
|
||||
<span class="line-title">Ensino superior público no Piemonte da Diamantina.</span>
|
||||
<span class="line-title">Hupes no SUS · articulação concluída em 2007.</span>
|
||||
<span class="line-meta"
|
||||
>Defesa registrada de campus da UFBA em Jacobina, junto com Jequié e Seabra.
|
||||
Alcance regional.
|
||||
<span class="source">INC 1497/2023 · Jornal da Chapada, 22/09/2023</span></span
|
||||
>
|
||||
</li>
|
||||
<li>
|
||||
<span class="line-title">Fortalecimento da estrutura regional de diagnóstico.</span>
|
||||
<span class="line-meta"
|
||||
>Registro de articulação pela ressonância da Policlínica de Jacobina; autoria da
|
||||
emenda atribuída a Amauri Teixeira.
|
||||
<span class="source">Marcelino Galo · 14/04/2018</span></span
|
||||
>A integração consolidou assistência, ensino e pesquisa no hospital universitário.
|
||||
<span class="source">Hupes/UFBA · 2007</span></span
|
||||
>
|
||||
</li>
|
||||
<li>
|
||||
<span class="line-title"
|
||||
>Promessa nominal para Miguel Calmon: sem registro localizado.</span
|
||||
>Rehuf · programa instituído em 2010 e regulamentado em 2011.</span
|
||||
>
|
||||
<span class="line-meta"
|
||||
>Não completar com proposta genérica nem apresentar ação regional como promessa
|
||||
municipal.</span
|
||||
>O financiamento compartilhado enfrentou o subfinanciamento dos hospitais
|
||||
universitários federais.
|
||||
<span class="source">Governo Federal · 2010–2011</span></span
|
||||
>
|
||||
</li>
|
||||
<li>
|
||||
<span class="line-title">Emendas 2015–2026 · fase declarada em cada valor.</span>
|
||||
<span class="line-meta"
|
||||
>O histórico separa autorizado, empenhado e pago; valores de fases diferentes não
|
||||
são somados nem apresentados como entrega concluída.
|
||||
<span class="source">Dossiê UFBA · consulta 2026</span></span
|
||||
>
|
||||
</li>
|
||||
<li>
|
||||
<span class="line-title"
|
||||
>Famed · restauro vinculado à recuperação do uso acadêmico.</span
|
||||
>
|
||||
<span class="line-meta"
|
||||
>O registro conecta preservação do prédio histórico a ensino, pesquisa e extensão.
|
||||
<span class="source">UFBA/Iphan · registros 2015–2026</span></span
|
||||
>
|
||||
</li>
|
||||
<li>
|
||||
<span class="line-title">Novo PAC · investimento anunciado em 2025.</span>
|
||||
<span class="line-meta"
|
||||
>Cite como anúncio do Governo Federal e informe a etapa registrada; não antecipe
|
||||
obra concluída nem atribua execução ao parlamentar.
|
||||
<span class="source">Novo PAC · 2025</span></span
|
||||
>
|
||||
</li>
|
||||
<li>
|
||||
<span class="line-title"
|
||||
>Defesa pública da UFBA · mobilização registrada em 2019.</span
|
||||
>
|
||||
<span class="line-meta"
|
||||
>A posição contra cortes e ataques à universidade é lastro político do recorte,
|
||||
não entrega material.
|
||||
<span class="source">Câmara dos Deputados · 2019</span></span
|
||||
>
|
||||
</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<section class="section">
|
||||
<p class="eyebrow">Roteiro curto · fale como pessoa, não como disparo</p>
|
||||
<h3 class="section-title">Faça o pedido com clareza</h3>
|
||||
<section class="request-block" aria-label="Pedido de voto">
|
||||
<p class="eyebrow">O pedido</p>
|
||||
<p class="request-line">
|
||||
“Posso contar com você? Para deputado federal, <em>vote 1313, Jorge Solla</em>.”
|
||||
Posso contar com você? Para deputado federal, <em>vote 1313, Jorge Solla.</em>
|
||||
</p>
|
||||
<p class="request-plan">
|
||||
<strong>Plano:</strong> combine onde, quando e como votar e registre o compromisso
|
||||
nomeado.
|
||||
</p>
|
||||
<ol class="script-list">
|
||||
<li>
|
||||
<span class="line-title">Comece pela relação.</span>
|
||||
<span class="line-meta"
|
||||
>“Quero conversar com você porque sua opinião importa para mim.” Escute antes de
|
||||
argumentar.</span
|
||||
>
|
||||
</li>
|
||||
<li>
|
||||
<span class="line-title">Use um fato do recorte.</span>
|
||||
<span class="line-meta"
|
||||
>Escolha um item da folha 1. Diga se é municipal ou regional e preserve o
|
||||
estágio.</span
|
||||
>
|
||||
</li>
|
||||
<li>
|
||||
<span class="line-title">Peça explicitamente: vote 1313.</span>
|
||||
<span class="line-meta"
|
||||
>Não pare em “pense com carinho”. Nome e número tornam o compromisso claro.</span
|
||||
>
|
||||
</li>
|
||||
<li>
|
||||
<span class="line-title">Combine o plano de voto: onde, quando e como.</span>
|
||||
<span class="line-meta"
|
||||
>“Você já sabe onde vota? Em que horário pretende ir? Como vai chegar?” Oriente
|
||||
sem pressionar.</span
|
||||
>
|
||||
</li>
|
||||
<li>
|
||||
<span class="line-title">Feche com compromisso nomeado.</span>
|
||||
<span class="line-meta"
|
||||
>“Então ficou combinado: para deputado federal, Jorge Solla, 1313?” Se houver
|
||||
dúvida, não force; combine retorno.</span
|
||||
>
|
||||
</li>
|
||||
</ol>
|
||||
</section>
|
||||
|
||||
<div class="rule-note">
|
||||
<strong>Por que assim:</strong> contato pessoal e relacional mobiliza melhor do que
|
||||
broadcast; escuta e narrativa vêm antes do argumento. Evidência internacional orienta o
|
||||
método, mas EUA ≠ Brasil — aqui o plano ajuda também a lembrar o número na urna.
|
||||
</div>
|
||||
|
||||
<footer class="brief-footer">
|
||||
<span
|
||||
>Base de capacitação: Nickerson & Rogers 2010 · Schein et al. 2021 · Kalla &
|
||||
Broockman 2020</span
|
||||
>
|
||||
<span>Briefing de capacitação · 2/4</span>
|
||||
<span><strong>Recorte:</strong> instituição · UFBA</span>
|
||||
<span>folha 2/4</span>
|
||||
</footer>
|
||||
</article>
|
||||
</section>
|
||||
|
||||
<section class="scene">
|
||||
<p class="scene-label">Folha 3/4 · Perguntas prováveis × melhores respostas</p>
|
||||
<article class="sheet" aria-label="Perguntas prováveis e melhores respostas">
|
||||
<p class="scene-label">Folha 3/4 · Perguntas prováveis × melhores respostas (1/2)</p>
|
||||
<article
|
||||
class="sheet"
|
||||
data-page="qa"
|
||||
aria-label="Perguntas prováveis e melhores respostas sobre a UFBA, parte 1"
|
||||
>
|
||||
<header class="brief-head">
|
||||
<div>
|
||||
<p class="kicker">Briefing de capacitação · Miguel Calmon</p>
|
||||
<p class="kicker">Briefing de capacitação · UFBA</p>
|
||||
<h2 class="brief-title">Perguntas prováveis × melhores respostas</h2>
|
||||
<p class="brief-subtitle">Reconhecer → fato local verificável → pedido</p>
|
||||
<p class="brief-subtitle">Parte 1 de 2 · posições e escolhas</p>
|
||||
</div>
|
||||
<p class="internal-label">
|
||||
Insumo interno de capacitação — não publicar
|
||||
INSUMO INTERNO DE CAPACITAÇÃO — NÃO PUBLICAR
|
||||
<span class="page-no">folha 3/4</span>
|
||||
</p>
|
||||
</header>
|
||||
|
||||
<div class="rule-note">
|
||||
<strong>Régua visível:</strong> reconheça a preocupação em uma frase, sem repetir o
|
||||
ataque; responda com um fato local verificável; feche perguntando se pode contar com o
|
||||
voto 1313. Se o dossiê não sustenta a resposta, diga que vai conferir.
|
||||
<strong>Régua:</strong> reconhecer sem repetir o ataque → responder com fato e fonte →
|
||||
fechar no pedido.
|
||||
</div>
|
||||
|
||||
<ol class="qa-list">
|
||||
<li class="qa-item">
|
||||
<p class="qa-question">
|
||||
<span class="qa-side">Objeção à direita</span>“Não voto no PT / não gosto do Lula.”
|
||||
<span class="qa-side">Objeção à direita</span>“Universidade pública virou lugar de
|
||||
partido.”
|
||||
</p>
|
||||
<p class="qa-answer">
|
||||
<b>Reconhecer:</b> “Entendo que a escolha partidária pesa.” <b>Fato:</b> em Miguel
|
||||
Calmon, há registros nominais de ações na saúde durante a gestão estadual, como
|
||||
Saúde em Movimento, ambulância e atenção domiciliar. <b>Fechar:</b> “Olhando esse
|
||||
trabalho no município, posso contar com seu voto 1313 para deputado federal?”
|
||||
<b>Reconhecer:</b> “Universidade deve servir à sociedade inteira.” <b>Fato:</b> no
|
||||
recorte da UFBA, a defesa de Solla liga autonomia a serviços concretos: formação,
|
||||
pesquisa e atendimento pelo Hupes no SUS.
|
||||
<span class="source">Hupes/UFBA · 2007</span> <b>Fechar:</b> “Por essa universidade
|
||||
pública a serviço do povo, posso contar com o 1313?”
|
||||
</p>
|
||||
</li>
|
||||
<li class="qa-item">
|
||||
<p class="qa-question">
|
||||
<span class="qa-side">Objeção à direita</span>“E o governo de Jerônimo? O que fez
|
||||
aqui?”
|
||||
<span class="qa-side">Objeção à direita</span>“Por que tirar educação do teto? Todo
|
||||
mundo precisa cortar.”
|
||||
</p>
|
||||
<p class="qa-answer">
|
||||
<b>Reconhecer:</b> “É justo cobrar o que chegou ao município.” <b>Fato:</b> este
|
||||
briefing não traz entrega do governo atual comprovada para responder isso; ele
|
||||
registra ações de outros períodos e iniciativas regionais. <b>Fechar:</b> “Posso
|
||||
conferir essa pergunta no dossiê e voltar? Sobre o trabalho documentado de Solla,
|
||||
posso pedir seu voto 1313?”
|
||||
<b>Reconhecer:</b> “Cobrar responsabilidade com dinheiro público é correto.”
|
||||
<b>Fato:</b> corte linear paralisa laboratório, aula e hospital; os registros
|
||||
mostram defesa de financiamento previsível para a universidade cumprir essas três
|
||||
funções. <span class="source">Registros legislativos · 2016–2023</span>
|
||||
<b>Fechar:</b> “Você apoia proteger esse investimento com o voto 1313?”
|
||||
</p>
|
||||
</li>
|
||||
<li class="qa-item">
|
||||
<p class="qa-question">
|
||||
<span class="qa-side">Objeção à esquerda</span>“Se o PT governa a Bahia, por que
|
||||
ainda falta tanto?”
|
||||
<span class="qa-side">Objeção à direita</span>“Cota não tira vaga de quem estudou
|
||||
mais?”
|
||||
</p>
|
||||
<p class="qa-answer">
|
||||
<b>Reconhecer:</b> “A cobrança é legítima; ação feita não apaga o que falta.”
|
||||
<b>Fato:</b> o levantamento registra serviços no município, mas também lacunas: não
|
||||
localizou emenda individual de Solla para Miguel Calmon nem obra estadual nominal na
|
||||
janela consultada. <b>Fechar:</b> “A proposta é cobrar sem esconder lacunas. Posso
|
||||
contar com seu voto 1313?”
|
||||
<b>Reconhecer:</b> “A preocupação com mérito e oportunidade é legítima.”
|
||||
<b>Fato:</b>
|
||||
a posição registrada combina cotas com permanência: abrir a porta e dar condição de
|
||||
concluir o curso, sem baixar a exigência acadêmica.
|
||||
<span class="source">Defesa da universidade pública · 2019</span> <b>Fechar:</b>
|
||||
“Essa combinação faz sentido para você? Posso contar com o 1313?”
|
||||
</p>
|
||||
</li>
|
||||
<li class="qa-item">
|
||||
<p class="qa-question">
|
||||
<span class="qa-side">Objeção à esquerda</span>“Por que falar de Jacobina se eu moro
|
||||
em Miguel Calmon?”
|
||||
<span class="qa-side">Objeção à esquerda</span>“Autonomia vale mesmo quando o
|
||||
governo discorda da eleição para reitor?”
|
||||
</p>
|
||||
<p class="qa-answer">
|
||||
<b>Reconhecer:</b> “Você tem razão em separar cidade e região.” <b>Fato:</b> campus
|
||||
da UFBA e Policlínica aparecem como iniciativas regionais em Jacobina; o material
|
||||
não as chama de entrega exclusiva a Miguel Calmon. <b>Fechar:</b> “Defender
|
||||
estrutura regional sem apagar o município faz sentido para você? Posso contar com o
|
||||
<b>Reconhecer:</b> “Autonomia só vale se valer também no conflito.”
|
||||
<b>Fato:</b> Solla registrou em 2019 a defesa do respeito à escolha da comunidade
|
||||
universitária, sem transformar a lista tríplice em veto político.
|
||||
<span class="source">Câmara dos Deputados · 2019</span> <b>Fechar:</b> “Para
|
||||
sustentar essa posição no Congresso, posso contar com o 1313?”
|
||||
</p>
|
||||
</li>
|
||||
<li class="qa-item">
|
||||
<p class="qa-question">
|
||||
<span class="qa-side">Objeção à esquerda</span>“Expandir campus sem permanência não
|
||||
é inclusão pela metade?”
|
||||
</p>
|
||||
<p class="qa-answer">
|
||||
<b>Reconhecer:</b> “É; matrícula sem condição de ficar não resolve.” <b>Fato:</b> as
|
||||
defesas do recorte tratam interiorização, cotas e assistência estudantil como partes
|
||||
da mesma política. <span class="source">Registros sobre educação · 2019–2024</span>
|
||||
<b>Fechar:</b> “Para cobrar expansão com permanência, posso contar com seu voto
|
||||
1313?”
|
||||
</p>
|
||||
</li>
|
||||
<li class="qa-item">
|
||||
<p class="qa-question">
|
||||
<span class="qa-side">Dúvida sobre entrega</span>“Ele prometeu o quê para Miguel
|
||||
Calmon?”
|
||||
</p>
|
||||
<p class="qa-answer">
|
||||
<b>Reconhecer:</b> “Promessa precisa estar registrada.” <b>Fato:</b> não foi
|
||||
localizada promessa nominal para o município; há defesa regional de campus da UFBA
|
||||
em Jacobina e registros de atuação em saúde. <b>Fechar:</b> “Não vou inventar
|
||||
promessa. Pelo trabalho comprovado, você considera votar 1313?”
|
||||
</p>
|
||||
</li>
|
||||
</ol>
|
||||
|
||||
<footer class="brief-footer">
|
||||
<span
|
||||
><strong>Não debata para vencer.</strong> Preserve a relação, responda o verificável e
|
||||
anote a lacuna.</span
|
||||
>
|
||||
<span>Briefing de capacitação · 3/4</span>
|
||||
<span><strong>Recorte:</strong> instituição · UFBA</span>
|
||||
<span>folha 3/4</span>
|
||||
</footer>
|
||||
</article>
|
||||
</section>
|
||||
|
||||
<section class="scene">
|
||||
<p class="scene-label">Folha 4/4 · O que evitar + conferência + limites</p>
|
||||
<article class="sheet sheet-compact" aria-label="O que evitar e o que conferir no dossiê">
|
||||
<p class="scene-label">Folha 4/4 · Perguntas prováveis × melhores respostas (2/2)</p>
|
||||
<article
|
||||
class="sheet"
|
||||
data-page="qa-2"
|
||||
aria-label="Perguntas prováveis e melhores respostas sobre a UFBA, continuação"
|
||||
>
|
||||
<header class="brief-head">
|
||||
<div>
|
||||
<p class="kicker">Briefing de capacitação · Miguel Calmon</p>
|
||||
<h2 class="brief-title">O que evitar</h2>
|
||||
<p class="brief-subtitle">
|
||||
Proteja a relação, a verdade do recorte e os limites do material
|
||||
</p>
|
||||
<p class="kicker">Briefing de capacitação · UFBA</p>
|
||||
<h2 class="brief-title">Perguntas prováveis × melhores respostas</h2>
|
||||
<p class="brief-subtitle">Continuação · parte 2 de 2 · entrega e atribuição</p>
|
||||
</div>
|
||||
<p class="internal-label">
|
||||
Insumo interno de capacitação — não publicar
|
||||
INSUMO INTERNO DE CAPACITAÇÃO — NÃO PUBLICAR
|
||||
<span class="page-no">folha 4/4</span>
|
||||
</p>
|
||||
</header>
|
||||
|
||||
<section class="section">
|
||||
<ul class="avoid-list">
|
||||
<li>
|
||||
<strong>Confrontar para ganhar a discussão.</strong> Em território hostil, encerre
|
||||
com respeito; não transforme contato em duelo.
|
||||
</li>
|
||||
<li>
|
||||
<strong>Envergonhar ou expor o eleitor.</strong> Pressão pesada gera reação. Convide
|
||||
ao compromisso sem constranger.
|
||||
</li>
|
||||
<li>
|
||||
<strong>Repetir o ataque ou o mito.</strong> Não lidere pela acusação. Reconheça a
|
||||
preocupação e ofereça uma explicação factual alternativa.
|
||||
</li>
|
||||
<li>
|
||||
<strong>Trocar conversa por broadcast impessoal.</strong> WhatsApp 1-a-1 e pessoa de
|
||||
confiança vêm antes de corrente ou disparo em massa.
|
||||
</li>
|
||||
<li>
|
||||
<strong>Prometer sem lastro.</strong> Indicação não é obra; articulação não é
|
||||
autoria; autorizado não é pago; região não é município.
|
||||
</li>
|
||||
</ul>
|
||||
</section>
|
||||
<ol class="qa-list">
|
||||
<li class="qa-item">
|
||||
<p class="qa-question">
|
||||
<span class="qa-side">Dúvida sobre entrega</span>“O que Solla entregou de concreto
|
||||
para a UFBA?”
|
||||
</p>
|
||||
<p class="qa-answer">
|
||||
<b>Reconhecer:</b> “A pergunta precisa separar atuação, recurso e execução.”
|
||||
<b>Fato:</b> o dossiê ancora a integração do Hupes ao SUS, o Rehuf e emendas entre
|
||||
2015 e 2026, sempre com autoria e fase explícitas.
|
||||
<span class="source">Dossiê UFBA · consulta 2026</span> <b>Fechar:</b> “Pelo
|
||||
conjunto documentado, posso contar com seu voto 1313?”
|
||||
</p>
|
||||
</li>
|
||||
<li class="qa-item">
|
||||
<p class="qa-question">
|
||||
<span class="qa-side">Dúvida sobre entrega</span>“A emenda foi anunciada ou o
|
||||
dinheiro chegou?”
|
||||
</p>
|
||||
<p class="qa-answer">
|
||||
<b>Reconhecer:</b> “Essa diferença muda a resposta.” <b>Fato:</b> cada valor do
|
||||
recorte vem acompanhado da fase — autorizado, empenhado ou pago — e só o que está
|
||||
registrado pode ser afirmado.
|
||||
<span class="source">Dossiê UFBA · registros 2015–2026</span> <b>Fechar:</b> “Com
|
||||
essa prestação de contas sem inflar entrega, posso contar com o 1313?”
|
||||
</p>
|
||||
</li>
|
||||
<li class="qa-item">
|
||||
<p class="qa-question">
|
||||
<span class="qa-side">Dúvida sobre entrega</span>“O Novo PAC é obra de Solla?”
|
||||
</p>
|
||||
<p class="qa-answer">
|
||||
<b>Reconhecer:</b> “Não é correto personalizar uma obra do Governo Federal.”
|
||||
<b>Fato:</b> o registro de 2025 deve ser citado como investimento anunciado pelo
|
||||
Novo PAC, com a etapa informada; articulação parlamentar não vira autoria da
|
||||
execução. <span class="source">Novo PAC · 2025</span> <b>Fechar:</b> “Para apoiar e
|
||||
fiscalizar esse investimento no Congresso, posso contar com o 1313?”
|
||||
</p>
|
||||
</li>
|
||||
<li class="qa-item">
|
||||
<p class="qa-question">
|
||||
<span class="qa-side">Objeção à esquerda</span>“Restaurar prédio histórico é
|
||||
prioridade com tanta falta de assistência?”
|
||||
</p>
|
||||
<p class="qa-answer">
|
||||
<b>Reconhecer:</b> “Patrimônio não pode competir com permanência estudantil.”
|
||||
<b>Fato:</b> a defesa da Famed é recuperar um espaço de ensino, pesquisa e extensão;
|
||||
ela não substitui custeio, bolsas ou assistência.
|
||||
<span class="source">Famed/UFBA · registros 2015–2026</span> <b>Fechar:</b> “Para
|
||||
defender as duas frentes sem falsa escolha, posso contar com o 1313?”
|
||||
</p>
|
||||
</li>
|
||||
<li class="qa-item">
|
||||
<p class="qa-question">
|
||||
<span class="qa-side">Dúvida sobre entrega</span>“Por que citar o Hupes se ele é da
|
||||
universidade?”
|
||||
</p>
|
||||
<p class="qa-answer">
|
||||
<b>Reconhecer:</b> “O hospital é patrimônio público da universidade; não é de um
|
||||
parlamentar.” <b>Fato:</b> o ponto do recorte é a atuação pela integração ao SUS em
|
||||
2007 e a defesa de financiamento para manter assistência, ensino e pesquisa juntos.
|
||||
<span class="source">Hupes/UFBA · 2007</span> <b>Fechar:</b> “Por essa defesa dos
|
||||
hospitais universitários no SUS, posso contar com seu voto 1313?”
|
||||
</p>
|
||||
</li>
|
||||
</ol>
|
||||
|
||||
<div class="two-column">
|
||||
<section>
|
||||
<p class="eyebrow">Antes de responder</p>
|
||||
<h3 class="section-title">O que conferir no dossiê</h3>
|
||||
<ul class="check-list">
|
||||
<li><strong>Fonte e data</strong> do fato que pretende usar.</li>
|
||||
<li><strong>Alcance:</strong> município, região ou polo.</li>
|
||||
<li><strong>Fase do valor:</strong> autorizado, empenhado, liquidado ou pago.</li>
|
||||
<li><strong>Autoria:</strong> quem apresentou, destinou ou executou.</li>
|
||||
<li>
|
||||
<strong>Lacuna:</strong> o que a busca não localizou e não pode ser inferido.
|
||||
</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<p class="eyebrow">Consulta em campo</p>
|
||||
<h3 class="section-title">Se você não souber</h3>
|
||||
<ul class="check-list">
|
||||
<li><strong>Diga com clareza:</strong> “Não tenho esse dado confirmado aqui.”</li>
|
||||
<li><strong>Anote a pergunta</strong> sem pedir dado pessoal desnecessário.</li>
|
||||
<li><strong>Combine retorno</strong> por canal 1-a-1, se a pessoa quiser.</li>
|
||||
<li><strong>Leve a lacuna</strong> à coordenação para conferência no dossiê.</li>
|
||||
<li>
|
||||
<strong>Não improvise</strong> número, promessa, fonte ou resposta do governo.
|
||||
</li>
|
||||
</ul>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<section class="limits">
|
||||
<p class="eyebrow">Limites e defeso</p>
|
||||
<p>
|
||||
Material interno de capacitação. Não publicar, encaminhar como peça, impulsionar nem
|
||||
adaptar sem revisão editorial e jurídica. Sem CTA público, marca de campanha, cenário
|
||||
eleitoral, estimativa de votos ou dado staff-only. Os fatos foram selecionados do
|
||||
dossiê de Miguel Calmon e devem ser conferidos na fonte antes de uso público.
|
||||
</p>
|
||||
<p>
|
||||
A evidência de mobilização citada no treinamento é majoritariamente internacional e
|
||||
não mede automaticamente o Brasil. Use os princípios para orientar contato pessoal,
|
||||
plano de voto e lembrança do número — nunca para prometer efeito ou misturar
|
||||
resultados de estudos diferentes.
|
||||
</p>
|
||||
</section>
|
||||
<p class="more-note">
|
||||
Se houver mais perguntas priorizadas: exibir “e mais N perguntas no briefing completo
|
||||
(.md)”.
|
||||
</p>
|
||||
|
||||
<footer class="brief-footer">
|
||||
<span>Anti-padrões: Lau et al. 2007 · Ecker et al. 2022 · Debunking Handbook 2020</span>
|
||||
<span>Briefing de capacitação · 4/4</span>
|
||||
<span><strong>Recorte:</strong> instituição · UFBA</span>
|
||||
<span>folha 4/4</span>
|
||||
</footer>
|
||||
</article>
|
||||
</section>
|
||||
|
|
|
|||
|
|
@ -1,13 +1,15 @@
|
|||
/**
|
||||
* Briefing de capacitação builder (C210): snapshot + per-era research + the
|
||||
* authored `<slug>.briefing.json` → one internal A4 PDF (fixed four sheets) and
|
||||
* the `.md` companion.
|
||||
* authored `<slug>.briefing.json` → one internal A4 PDF (four fixed recorte
|
||||
* sheets: defesas → essencial + pedido → qa 1/2 → qa 2/2) and the `.md`
|
||||
* companion.
|
||||
*
|
||||
* Runs on the workstation, offline: no database access, no official-source
|
||||
* fetch — the briefing derives from the dossiê JSONs already researched, and
|
||||
* every anchor must resolve in the `bulletinFacts` ledger (facts with source).
|
||||
* The four-page cap is a hard guard: on overflow the lowest-priority lists shed
|
||||
* by priority (declared on the sheet, complete in the `.md`), never silently.
|
||||
* The four-sheet cap is a hard guard: on overflow the lowest-priority lists shed
|
||||
* by priority (qa → defesas; declared on the sheet, complete in the `.md`),
|
||||
* never silently.
|
||||
*
|
||||
* Usage:
|
||||
* NODE_OPTIONS="--no-deprecation --import=tsx/esm" node scripts/build-dossie-solla-briefing.mjs \
|
||||
|
|
@ -147,9 +149,9 @@ try {
|
|||
maxPages: BRIEFING_PAGE_TOTAL,
|
||||
requiredAnchors: BRIEFING_ANCHORS,
|
||||
label: 'O briefing',
|
||||
// Four fixed sheets: on overflow the lowest-priority list sheds one item
|
||||
// per pass (qa → defesas → conferir → evitar) until it fits; the remainder
|
||||
// stays counted on the sheet and complete in the `.md`.
|
||||
// Four fixed recorte sheets: on overflow the lowest-priority list sheds one
|
||||
// item per pass (qa → defesas) until it fits; the remainder stays counted on
|
||||
// the sheet and complete in the `.md`.
|
||||
onOverflow: () => {
|
||||
const trimmed = trimBriefing(printContent)
|
||||
if (!trimmed) return null
|
||||
|
|
|
|||
|
|
@ -7,25 +7,22 @@
|
|||
* dossiê ledger already carries (`buildDossierReport().bulletinFacts`), never at
|
||||
* new research. This module owns the fail-closed validation, the deny-list that
|
||||
* keeps staff-only/scenario keys out and the deterministic shed order that keeps
|
||||
* the four-page cap honest: the PDF prints at most four fixed sheets, so on
|
||||
* overflow the lowest-priority lists lose items (declared, never silent) and
|
||||
* everything stays in the companion `.md`.
|
||||
* the four-sheet cap honest: the PDF prints four fixed recorte-only sheets
|
||||
* (defesas → essencial + pedido → qa 1/2 → qa 2/2), so on overflow the
|
||||
* lowest-priority lists lose items (declared, never silent) and everything stays
|
||||
* in the companion `.md`.
|
||||
*/
|
||||
|
||||
export const BRIEFING_LABEL = 'Insumo interno de capacitação — não publicar'
|
||||
export const BRIEFING_PAGE_TOTAL = 4
|
||||
export const BRIEFING_ANCHORS = ['essencial', 'defesas', 'qa', 'evitar']
|
||||
export const BRIEFING_ANCHORS = ['defesas', 'essencial', 'qa', 'qa-2']
|
||||
export const BRIEFING_QA_SIDES = ['direita', 'esquerda', 'entrega']
|
||||
/** Sides the briefing must always print at least once (C210 guardrail). */
|
||||
const REQUIRED_QA_SIDES = ['direita', 'esquerda']
|
||||
|
||||
export const BRIEFING_MINIMUMS = {
|
||||
essential: 3,
|
||||
scriptSteps: 3,
|
||||
qa: 4,
|
||||
avoid: 3,
|
||||
beforeAnswer: 2,
|
||||
unsure: 2,
|
||||
}
|
||||
|
||||
/** Staff-only electoral scenarios never enter the handout (intention guardrail). */
|
||||
|
|
@ -50,15 +47,11 @@ const TEXT_CAPS = {
|
|||
essentialNote: 200,
|
||||
defenseTitle: 120,
|
||||
defenseNote: 200,
|
||||
scriptTitle: 90,
|
||||
scriptNote: 220,
|
||||
plan: 220,
|
||||
qaQuestion: 180,
|
||||
qaAcknowledge: 200,
|
||||
qaAnswer: 520,
|
||||
qaClose: 200,
|
||||
avoidTitle: 120,
|
||||
avoidNote: 200,
|
||||
checklistItem: 200,
|
||||
}
|
||||
|
||||
const isNonEmptyString = (value) => typeof value === 'string' && value.trim() !== ''
|
||||
|
|
@ -107,18 +100,34 @@ const warnIfOver = (warnings, text, max, label) => {
|
|||
* bulletin already refuses it a printed slot, so the briefing refuses it an
|
||||
* anchor: an "essencial" item is never a speech sample.
|
||||
*
|
||||
* A checklist id repeats across items (the research file has several items per
|
||||
* id), so a bare `factId` resolves to the first item — the legacy behavior. To
|
||||
* anchor a specific item of a repeated id the item carries the research item's
|
||||
* `sourceUrl` as a disambiguator: the anchor only counts when the pair
|
||||
* (`factId`, `sourceUrl`) resolves in the same sourced fact. Wrong pair = null
|
||||
* and the caller fails closed.
|
||||
*
|
||||
* @param {any[]} facts
|
||||
* @param {string} factId
|
||||
* @param {string|null} [sourceUrl]
|
||||
* @returns {any|null}
|
||||
*/
|
||||
export const briefingAnchorFact = (facts, factId) =>
|
||||
(Array.isArray(facts) ? facts : []).find((fact) => fact?.id === factId && !fact.sourcePanel) ??
|
||||
null
|
||||
export const briefingAnchorFact = (facts, factId, sourceUrl = null) => {
|
||||
const candidates = (Array.isArray(facts) ? facts : []).filter(
|
||||
(fact) => fact?.id === factId && !fact.sourcePanel,
|
||||
)
|
||||
if (isNonEmptyString(sourceUrl)) {
|
||||
return candidates.find((fact) => fact.sourceUrl === sourceUrl) ?? null
|
||||
}
|
||||
return candidates[0] ?? null
|
||||
}
|
||||
|
||||
/**
|
||||
* The anchor rule of every item: exactly one of `factId | gapReason`, and a
|
||||
* `factId` only counts when it resolves in a sourced, non-panel ledger fact.
|
||||
* Returns the resolved anchor; `normalizeAnchor` adds the copy validation.
|
||||
* When the id repeats, the item's optional `sourceUrl` (copied from the research
|
||||
* item) picks the exact fact. Returns the resolved anchor; `normalizeAnchor`
|
||||
* adds the copy validation.
|
||||
*/
|
||||
const resolveAnchor = (item, { facts, label }) => {
|
||||
const hasFact = isNonEmptyString(item.factId)
|
||||
|
|
@ -127,10 +136,13 @@ const resolveAnchor = (item, { facts, label }) => {
|
|||
fail(`${label}: exige exatamente um de "factId" | "gapReason".`)
|
||||
}
|
||||
if (hasGap) return { factId: null, gapReason: item.gapReason.trim(), fact: null }
|
||||
const fact = briefingAnchorFact(facts, item.factId)
|
||||
const sourceUrl = isNonEmptyString(item.sourceUrl) ? item.sourceUrl.trim() : null
|
||||
const fact = briefingAnchorFact(facts, item.factId, sourceUrl)
|
||||
if (!fact?.sourceUrl) {
|
||||
fail(
|
||||
`${label}: factId "${item.factId}" não resolve em fato com fonte do dossiê — sem fonte, não entra.`,
|
||||
`${label}: factId "${item.factId}"${
|
||||
sourceUrl ? ` + sourceUrl "${sourceUrl}"` : ''
|
||||
} não resolve em fato com fonte do dossiê — sem fonte, não entra.`,
|
||||
)
|
||||
}
|
||||
return { factId: fact.id, gapReason: null, fact }
|
||||
|
|
@ -175,24 +187,15 @@ const normalizeDefenses = (raw, options) =>
|
|||
}),
|
||||
)
|
||||
|
||||
const normalizeScript = (raw, options) => {
|
||||
const steps = raw.script?.steps ?? []
|
||||
assertList(steps, 'script.steps')
|
||||
assertMinimum(steps, BRIEFING_MINIMUMS.scriptSteps, 'script.steps')
|
||||
return {
|
||||
steps: steps.map((step, index) => {
|
||||
assertKey(step, 'title', `script.steps[${index}]`)
|
||||
assertKey(step, 'note', `script.steps[${index}]`)
|
||||
warnIfOver(
|
||||
options.warnings,
|
||||
step.title,
|
||||
TEXT_CAPS.scriptTitle,
|
||||
`script.steps[${index}].title`,
|
||||
)
|
||||
warnIfOver(options.warnings, step.note, TEXT_CAPS.scriptNote, `script.steps[${index}].note`)
|
||||
return { title: step.title.trim(), note: step.note.trim() }
|
||||
}),
|
||||
}
|
||||
/**
|
||||
* The vote plan is one line of copy under the literal request — the generic
|
||||
* five-step roteiro was retired (C210 replan): the sheet prints the literal ask
|
||||
* and this single line, nothing else.
|
||||
*/
|
||||
const normalizePlan = (raw, options) => {
|
||||
assertKey(raw, 'plan', 'briefing.json')
|
||||
warnIfOver(options.warnings, raw.plan, TEXT_CAPS.plan, 'plan')
|
||||
return raw.plan.trim()
|
||||
}
|
||||
|
||||
const normalizeQa = (raw, options) => {
|
||||
|
|
@ -212,7 +215,7 @@ const normalizeQa = (raw, options) => {
|
|||
warnIfOver(options.warnings, item.answer, TEXT_CAPS.qaAnswer, `${label}.answer`)
|
||||
warnIfOver(options.warnings, item.close, TEXT_CAPS.qaClose, `${label}.close`)
|
||||
const anchor = resolveAnchor(
|
||||
{ factId: item.factId, gapReason: item.gapReason },
|
||||
{ factId: item.factId, gapReason: item.gapReason, sourceUrl: item.sourceUrl },
|
||||
{ facts: options.facts, label },
|
||||
)
|
||||
return {
|
||||
|
|
@ -231,38 +234,6 @@ const normalizeQa = (raw, options) => {
|
|||
return items
|
||||
}
|
||||
|
||||
const normalizeAvoid = (raw, options) => {
|
||||
const avoid = raw.avoid ?? []
|
||||
assertList(avoid, 'avoid')
|
||||
assertMinimum(avoid, BRIEFING_MINIMUMS.avoid, 'avoid')
|
||||
return avoid.map((item, index) => {
|
||||
assertKey(item, 'title', `avoid[${index}]`)
|
||||
assertKey(item, 'note', `avoid[${index}]`)
|
||||
warnIfOver(options.warnings, item.title, TEXT_CAPS.avoidTitle, `avoid[${index}].title`)
|
||||
warnIfOver(options.warnings, item.note, TEXT_CAPS.avoidNote, `avoid[${index}].note`)
|
||||
return { title: item.title.trim(), note: item.note.trim() }
|
||||
})
|
||||
}
|
||||
|
||||
const normalizeChecklist = (raw, options) => {
|
||||
const beforeAnswer = raw.checklist?.beforeAnswer ?? []
|
||||
const unsure = raw.checklist?.unsure ?? []
|
||||
assertList(beforeAnswer, 'checklist.beforeAnswer')
|
||||
assertList(unsure, 'checklist.unsure')
|
||||
assertMinimum(beforeAnswer, BRIEFING_MINIMUMS.beforeAnswer, 'checklist.beforeAnswer')
|
||||
assertMinimum(unsure, BRIEFING_MINIMUMS.unsure, 'checklist.unsure')
|
||||
const items = (list, label) =>
|
||||
list.map((value, index) => {
|
||||
if (!isNonEmptyString(value)) fail(`${label}[${index}]: item não vazio.`)
|
||||
warnIfOver(options.warnings, value, TEXT_CAPS.checklistItem, `${label}[${index}]`)
|
||||
return value.trim()
|
||||
})
|
||||
return {
|
||||
beforeAnswer: items(beforeAnswer, 'checklist.beforeAnswer'),
|
||||
unsure: items(unsure, 'checklist.unsure'),
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fail-closed normalization of the authored briefing. Every anchor must resolve
|
||||
* in the sourced ledger; a lacuna is declared with `gapReason`, never invented.
|
||||
|
|
@ -302,11 +273,9 @@ export const normalizeBriefingContent = (raw, { unit, slug, facts = [] }) => {
|
|||
lede: raw.lede.trim(),
|
||||
essential,
|
||||
defenses: normalizeDefenses(raw, { facts, warnings }),
|
||||
script: normalizeScript(raw, { facts, warnings }),
|
||||
plan: normalizePlan(raw, { warnings }),
|
||||
qa: normalizeQa(raw, { facts, warnings }),
|
||||
avoid: normalizeAvoid(raw, { facts, warnings }),
|
||||
checklist: normalizeChecklist(raw, { facts, warnings }),
|
||||
shed: { qa: 0, defenses: 0, checklist: 0, avoid: 0 },
|
||||
shed: { qa: 0, defenses: 0 },
|
||||
warnings,
|
||||
}
|
||||
}
|
||||
|
|
@ -328,10 +297,10 @@ const qaShedIndex = (qa) => {
|
|||
|
||||
/**
|
||||
* One shed step: drop a single item from the lowest-priority list that is still
|
||||
* above its minimum (qa → defesas → conferir → evitar), never breaking the
|
||||
* two-sides coverage. Essential, roteiro and identification never shed. Returns
|
||||
* a new content object, or `null` when nothing else may be dropped — the caller
|
||||
* then fails closed.
|
||||
* above its minimum (qa → defesas), never breaking the two-sides coverage.
|
||||
* Essential, the principles copy and the pedido never shed. Returns a new
|
||||
* content object, or `null` when nothing else may be dropped — the caller then
|
||||
* fails closed.
|
||||
*
|
||||
* @param {any} content
|
||||
* @returns {any|null}
|
||||
|
|
@ -353,28 +322,5 @@ export const trimBriefing = (content) => {
|
|||
shed.defenses += 1
|
||||
return { ...content, defenses: content.defenses.slice(0, -1), shed }
|
||||
}
|
||||
const checklistTotal = content.checklist.beforeAnswer.length + content.checklist.unsure.length
|
||||
if (checklistTotal > BRIEFING_MINIMUMS.beforeAnswer + BRIEFING_MINIMUMS.unsure) {
|
||||
shed.checklist += 1
|
||||
if (content.checklist.unsure.length > BRIEFING_MINIMUMS.unsure) {
|
||||
return {
|
||||
...content,
|
||||
checklist: { ...content.checklist, unsure: content.checklist.unsure.slice(0, -1) },
|
||||
shed,
|
||||
}
|
||||
}
|
||||
return {
|
||||
...content,
|
||||
checklist: {
|
||||
...content.checklist,
|
||||
beforeAnswer: content.checklist.beforeAnswer.slice(0, -1),
|
||||
},
|
||||
shed,
|
||||
}
|
||||
}
|
||||
if (content.avoid.length > BRIEFING_MINIMUMS.avoid) {
|
||||
shed.avoid += 1
|
||||
return { ...content, avoid: content.avoid.slice(0, -1), shed }
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,9 +3,11 @@
|
|||
*
|
||||
* Ports the approved gate design (`docs/plans/briefing-capacitacao-solla-ui-design.html`)
|
||||
* class by class into the inline print CSS the report family uses: four fixed
|
||||
* A4 sheets (`essencial|defesas|qa|evitar`), sober, no campaign brand, the
|
||||
* literal internal label on every sheet and the vote request as a rendered
|
||||
* literal. The companion `.md` carries everything the PDF may have shed.
|
||||
* A4 sheets, all of them recorte content — `defesas` (princípios + posições com
|
||||
* fonte), `essencial` (fatos-âncora + o pedido), `qa` and `qa-2` (perguntas
|
||||
* prováveis, 1/2 e 2/2). Sober, no campaign brand, the literal internal label on
|
||||
* every sheet and the vote request as a rendered literal. The companion `.md`
|
||||
* carries everything the PDF may have shed.
|
||||
*/
|
||||
|
||||
import { BRIEFING_LABEL, BRIEFING_PAGE_TOTAL } from './briefingContent.mjs'
|
||||
|
|
@ -14,7 +16,7 @@ import { resolveDossierUnit } from './dossieUnit.mjs'
|
|||
import { htmlEscape, moreItemsLabel } from './reportText.mjs'
|
||||
|
||||
const BRIEFING_REQUEST_LINE =
|
||||
'“Posso contar com você? Para deputado federal, <em>vote 1313, Jorge Solla</em>.”'
|
||||
'Posso contar com você? Para deputado federal, <em>vote 1313, Jorge Solla.</em>'
|
||||
|
||||
const BRIEFING_PRINT_CSS = `
|
||||
:root {
|
||||
|
|
@ -55,58 +57,32 @@ const BRIEFING_PRINT_CSS = `
|
|||
.section { margin-top: 5mm; }
|
||||
.section + .section { margin-top: 6mm; }
|
||||
.section-title { margin: 1.2mm 0 0; font-size: 13.5pt; font-weight: 650; line-height: 1.15; letter-spacing: -.015em; }
|
||||
.lede { max-width: 158mm; margin: 3mm 0 0; font-size: 10.2pt; line-height: 1.5; }
|
||||
.principles-copy { max-width: 162mm; margin: 3mm 0 0; font-size: 10.2pt; line-height: 1.5; }
|
||||
.rule-note { margin-top: 4mm; padding: 2.5mm 0 2.5mm 4mm; border-top: .25mm solid var(--line); border-bottom: .25mm solid var(--line); border-left: 1mm solid var(--accent); color: var(--ink-soft); font-size: 8.4pt; line-height: 1.42; }
|
||||
.rule-note strong { color: var(--ink); }
|
||||
.line-list, .script-list, .avoid-list, .check-list { margin: 3mm 0 0; padding: 0; border-top: .35mm solid var(--ink); list-style: none; }
|
||||
.line-list li, .script-list li, .avoid-list li, .check-list li { position: relative; padding: 2.5mm 0 2.5mm 6mm; border-bottom: .2mm solid var(--line-light); }
|
||||
.line-list li::before, .script-list li::before, .avoid-list li::before, .check-list li::before { position: absolute; top: 3.7mm; left: 0; width: 2.2mm; height: .45mm; background: var(--accent); content: ''; }
|
||||
.line-list { margin: 3mm 0 0; padding: 0; border-top: .35mm solid var(--ink); list-style: none; }
|
||||
.line-list li { position: relative; padding: 2.35mm 0 2.35mm 6mm; border-bottom: .2mm solid var(--line-light); }
|
||||
.line-list li::before { position: absolute; top: 3.55mm; left: 0; width: 2.2mm; height: .45mm; background: var(--accent); content: ''; }
|
||||
.line-title { display: block; font-weight: 700; }
|
||||
.line-meta { display: block; margin-top: .8mm; color: var(--ink-soft); font-size: 7.8pt; line-height: 1.4; }
|
||||
.more-note { margin: 3mm 0 0; color: var(--ink-soft); font-size: 8pt; font-weight: 600; line-height: 1.4; }
|
||||
.line-meta { display: block; margin-top: .7mm; color: var(--ink-soft); font-size: 7.8pt; line-height: 1.36; }
|
||||
.source { color: var(--accent); font-size: 7.4pt; font-weight: 700; }
|
||||
.request-line { margin: 3.5mm 0 0; padding: 4mm 0; border-top: .6mm solid var(--ink); border-bottom: .6mm solid var(--ink); font-size: 14pt; font-weight: 650; line-height: 1.3; letter-spacing: -.015em; }
|
||||
.more-note { margin: 3mm 0 0; color: var(--ink-soft); font-size: 8pt; font-weight: 600; line-height: 1.4; }
|
||||
.request-block { margin-top: 5mm; padding: 4.5mm 5mm 4mm; border: .35mm solid var(--line); border-left: 1.2mm solid var(--accent); background: var(--note); }
|
||||
.request-line { margin: 2mm 0 0; font-size: 14pt; font-weight: 650; line-height: 1.3; letter-spacing: -.015em; }
|
||||
.request-line em { color: var(--accent); font-style: normal; }
|
||||
.script-list { counter-reset: script-step; }
|
||||
.script-list li { min-height: 14mm; padding-left: 13mm; counter-increment: script-step; }
|
||||
.script-list li::before { top: 2.7mm; width: 8mm; height: auto; color: var(--accent); background: none; content: counter(script-step, decimal-leading-zero); font-size: 8pt; font-weight: 700; }
|
||||
.request-plan { margin: 3mm 0 0; padding-top: 2.5mm; border-top: .25mm solid var(--line); color: var(--ink-soft); font-size: 8.6pt; line-height: 1.4; }
|
||||
.request-plan strong { color: var(--ink); }
|
||||
.qa-list { margin: 3mm 0 0; padding: 0; border-top: .35mm solid var(--ink); list-style: none; }
|
||||
.qa-item { display: grid; padding: 3.2mm 0; border-bottom: .2mm solid var(--line-light); grid-template-columns: 46mm 1fr; gap: 5mm; }
|
||||
.qa-question { margin: 0; font-size: 8.6pt; font-weight: 700; line-height: 1.38; }
|
||||
.qa-side { display: block; margin-bottom: 1mm; color: var(--accent); font-size: 6.8pt; letter-spacing: .07em; text-transform: uppercase; }
|
||||
.qa-answer { margin: 0; font-size: 8.6pt; line-height: 1.45; }
|
||||
.qa-answer b { font-weight: 700; }
|
||||
.avoid-list strong, .check-list strong { font-weight: 700; }
|
||||
.two-column { display: grid; margin-top: 5mm; grid-template-columns: 1fr 1fr; gap: 8mm; }
|
||||
.limits { margin-top: 5mm; padding-top: 3mm; border-top: .6mm solid var(--ink); }
|
||||
.limits p { margin: 1.5mm 0 0; color: var(--ink-soft); font-size: 8pt; line-height: 1.42; }
|
||||
.brief-footer { display: flex; margin-top: auto; padding-top: 2.5mm; border-top: .25mm solid var(--line); justify-content: space-between; gap: 8mm; color: var(--ink-soft); font-size: 7.2pt; line-height: 1.3; }
|
||||
.brief-footer strong { color: var(--ink); }
|
||||
.sheet-compact .section { margin-top: 3.8mm; }
|
||||
.sheet-compact .avoid-list, .sheet-compact .check-list { margin-top: 2mm; }
|
||||
.sheet-compact .avoid-list li, .sheet-compact .check-list li { padding-top: 1.65mm; padding-bottom: 1.65mm; font-size: 8.7pt; line-height: 1.32; }
|
||||
.sheet-compact .avoid-list li::before, .sheet-compact .check-list li::before { top: 2.8mm; }
|
||||
.sheet-compact .two-column { margin-top: 3.8mm; gap: 6mm; }
|
||||
.sheet-compact .section-title { font-size: 12.5pt; }
|
||||
.sheet-compact .limits { margin-top: 3.5mm; padding-top: 2mm; }
|
||||
.sheet-compact .limits p { margin-top: 1mm; font-size: 7.5pt; line-height: 1.32; }
|
||||
.sheet-compact .brief-footer { padding-top: 1.8mm; }
|
||||
`
|
||||
|
||||
const HOW_TO_USE_COPY =
|
||||
'Antes: escolha dois fatos e ensaie o pedido. Durante: escute, responda com um fato verificável e peça o voto. Depois: registre a dúvida que ficou sem resposta para o próximo ciclo do dossiê.'
|
||||
|
||||
const WHY_COPY =
|
||||
'contato pessoal e relacional mobiliza melhor do que broadcast; escuta e narrativa vêm antes do argumento. Evidência internacional orienta o método, mas EUA ≠ Brasil — aqui o plano ajuda também a lembrar o número na urna.'
|
||||
|
||||
const RULER_COPY =
|
||||
'reconheça a preocupação em uma frase, sem repetir o ataque; responda com um fato local verificável; feche perguntando se pode contar com o voto 1313. Se o dossiê não sustenta a resposta, diga que vai conferir.'
|
||||
|
||||
const limitsCopyA = (name) =>
|
||||
`Material interno de capacitação. Não publicar, encaminhar como peça, impulsionar nem adaptar sem revisão editorial e jurídica. Sem CTA público, marca de campanha, cenário eleitoral, estimativa de votos ou dado staff-only. Os fatos foram selecionados do dossiê de ${name} e devem ser conferidos na fonte antes de uso público.`
|
||||
|
||||
const LIMITS_COPY_B =
|
||||
'A evidência de mobilização citada no treinamento é majoritariamente internacional e não mede automaticamente o Brasil. Use os princípios para orientar contato pessoal, plano de voto e lembrança do número — nunca para prometer efeito ou misturar resultados de estudos diferentes.'
|
||||
const RULER_COPY = 'reconheça sem repetir o ataque → responda com fato e fonte → feche no pedido.'
|
||||
|
||||
const QA_SIDE_LABELS = {
|
||||
direita: 'Objeção à direita',
|
||||
|
|
@ -122,8 +98,6 @@ const QA_SIDE_LABELS = {
|
|||
export const briefingSubjectName = (report, content) =>
|
||||
report?.meta?.municipalityName ?? report?.meta?.subjectName ?? content?.slug ?? '—'
|
||||
|
||||
const inlineBold = (value) => htmlEscape(value).replace(/\*\*([^*]+)\*\*/g, '<strong>$1</strong>')
|
||||
|
||||
const anchorHtml = (item) =>
|
||||
item.fact
|
||||
? `<a class="source" href="${htmlEscape(item.fact.sourceUrl)}">fonte${
|
||||
|
|
@ -136,10 +110,10 @@ const anchorMd = (item) =>
|
|||
? `[fonte](${item.fact.sourceUrl})${item.fact.sourceDate ? ` · ${formatDateBr(item.fact.sourceDate)}` : ''}`
|
||||
: `_lacuna: ${item.gapReason}_`
|
||||
|
||||
const headerHtml = (unit, title, subtitle, page, kickerSubject) => `
|
||||
const headerHtml = (kickerSubject, title, subtitle, page) => `
|
||||
<header class="brief-head">
|
||||
<div>
|
||||
<p class="kicker">Briefing de capacitação · ${htmlEscape(kickerSubject ?? unit.briefingNoun ?? unit.id)}</p>
|
||||
<p class="kicker">Briefing de capacitação · ${htmlEscape(kickerSubject)}</p>
|
||||
<h2 class="brief-title">${htmlEscape(title)}</h2>
|
||||
<p class="brief-subtitle">${htmlEscape(subtitle)}</p>
|
||||
</div>
|
||||
|
|
@ -149,8 +123,8 @@ const headerHtml = (unit, title, subtitle, page, kickerSubject) => `
|
|||
</p>
|
||||
</header>`
|
||||
|
||||
const footerHtml = (left, page) =>
|
||||
`<footer class="brief-footer"><span>${left}</span><span>Briefing de capacitação · ${page}/${BRIEFING_PAGE_TOTAL}</span></footer>`
|
||||
const footerHtml = (noun, name, page) =>
|
||||
`<footer class="brief-footer"><span><strong>Recorte:</strong> ${htmlEscape(noun)} · ${htmlEscape(name)}</span><span>folha ${page}/${BRIEFING_PAGE_TOTAL}</span></footer>`
|
||||
|
||||
const moreNote = (count, singular, plural) =>
|
||||
count > 0
|
||||
|
|
@ -165,25 +139,17 @@ const lineItemsHtml = (items) =>
|
|||
)
|
||||
.join('\n')
|
||||
|
||||
const howToUseNoteHtml = () =>
|
||||
`<div class="rule-note"><strong>Como usar.</strong> ${htmlEscape(HOW_TO_USE_COPY)}</div>`
|
||||
const qaItemsHtml = (items) =>
|
||||
(items ?? [])
|
||||
.map(
|
||||
(item) => `<li class="qa-item">
|
||||
<p class="qa-question"><span class="qa-side">${htmlEscape(QA_SIDE_LABELS[item.side] ?? item.side)}</span>${htmlEscape(item.question)}</p>
|
||||
<p class="qa-answer"><b>Reconhecer:</b> ${htmlEscape(item.acknowledge)} <b>Fato:</b> ${htmlEscape(item.answer)} ${anchorHtml(item)} <b>Fechar:</b> ${htmlEscape(item.close)}</p>
|
||||
</li>`,
|
||||
)
|
||||
.join('\n')
|
||||
|
||||
const sheetEssential = (content, { unit, report }) => `
|
||||
<article class="sheet" data-page="essencial" aria-label="O essencial do briefing de ${htmlEscape(briefingSubjectName(report, content))}">
|
||||
${headerHtml(unit, briefingSubjectName(report, content), content.subtitle ?? 'Consulta antes e durante o contato', 1, unit.briefingNoun ?? unit.id)}
|
||||
<section class="section">
|
||||
<p class="eyebrow">Comece por aqui</p>
|
||||
<h3 class="section-title">O essencial do recorte</h3>
|
||||
<p class="lede">${htmlEscape(content.lede)}</p>
|
||||
<ul class="line-list">
|
||||
${lineItemsHtml(content.essential)}
|
||||
</ul>
|
||||
</section>
|
||||
${howToUseNoteHtml()}
|
||||
${footerHtml(`<strong>Regra:</strong> ${unit.sumGuardNote ? `${htmlEscape(unit.sumGuardNote)} · ` : ''}fase acompanha valor · sem fonte, não afirmar`, 1)}
|
||||
</article>`
|
||||
|
||||
const sheetDefenses = (content, { unit, report }) => {
|
||||
const sheetDefenses = (content, { name, noun }) => {
|
||||
const defenses =
|
||||
content.defenses.length > 0
|
||||
? lineItemsHtml(content.defenses)
|
||||
|
|
@ -191,86 +157,55 @@ const sheetDefenses = (content, { unit, report }) => {
|
|||
? ''
|
||||
: '<li><span class="line-title">Sem registro localizado no dossiê.</span><span class="line-meta">Não completar por memória nem apresentar ação regional como promessa do recorte. Confira no dossiê antes de afirmar.</span></li>'
|
||||
return `
|
||||
<article class="sheet" data-page="defesas" aria-label="O que Solla defende e roteiro do pedido de voto">
|
||||
${headerHtml(unit, 'Defesas e pedido de voto', 'Do fato local ao compromisso nomeado', 2, briefingSubjectName(report, content))}
|
||||
<article class="sheet" data-page="defesas" aria-label="O que Solla defende em ${htmlEscape(name)}">
|
||||
${headerHtml(noun, 'O que Solla defende', `${name} · princípios que orientam posição e voto`, 1)}
|
||||
<section class="section">
|
||||
<p class="eyebrow">Leitura dos registros · não é opinião</p>
|
||||
<h3 class="section-title">O que Solla defende</h3>
|
||||
<p class="eyebrow">Princípios e crenças · leitura dos registros, não é opinião</p>
|
||||
<p class="principles-copy">${htmlEscape(content.lede)}</p>
|
||||
<ul class="line-list">
|
||||
${defenses}
|
||||
</ul>
|
||||
${moreNote(content.shed.defenses, 'defesa', 'defesas')}
|
||||
</section>
|
||||
<section class="section">
|
||||
<p class="eyebrow">Roteiro curto · fale como pessoa, não como disparo</p>
|
||||
<h3 class="section-title">Faça o pedido com clareza</h3>
|
||||
<p class="request-line">${BRIEFING_REQUEST_LINE}</p>
|
||||
<ol class="script-list">
|
||||
${content.script.steps
|
||||
.map(
|
||||
(step) =>
|
||||
`<li><span class="line-title">${htmlEscape(step.title)}</span><span class="line-meta">${htmlEscape(step.note)}</span></li>`,
|
||||
)
|
||||
.join('\n')}
|
||||
</ol>
|
||||
</section>
|
||||
<div class="rule-note"><strong>Por que assim:</strong> ${htmlEscape(WHY_COPY)}</div>
|
||||
${footerHtml(`Base de capacitação: Nickerson & Rogers 2010 · Schein et al. 2021 · Kalla & Broockman 2020`, 2)}
|
||||
${footerHtml(noun, name, 1)}
|
||||
</article>`
|
||||
}
|
||||
|
||||
const sheetQa = (content, { unit, report }) => `
|
||||
<article class="sheet" data-page="qa" aria-label="Perguntas prováveis e melhores respostas">
|
||||
${headerHtml(unit, 'Perguntas prováveis × melhores respostas', 'Reconhecer → fato local verificável → pedido', 3, briefingSubjectName(report, content))}
|
||||
<div class="rule-note"><strong>Régua visível:</strong> ${htmlEscape(RULER_COPY)}</div>
|
||||
<ol class="qa-list">
|
||||
${content.qa
|
||||
.map(
|
||||
(item) => `<li class="qa-item">
|
||||
<p class="qa-question"><span class="qa-side">${htmlEscape(QA_SIDE_LABELS[item.side] ?? item.side)}</span>${htmlEscape(item.question)}</p>
|
||||
<p class="qa-answer"><b>Reconhecer:</b> ${htmlEscape(item.acknowledge)} <b>Fato:</b> ${htmlEscape(item.answer)} ${anchorHtml(item)} <b>Fechar:</b> ${htmlEscape(item.close)}</p>
|
||||
</li>`,
|
||||
)
|
||||
.join('\n')}
|
||||
</ol>
|
||||
${moreNote(content.shed.qa, 'pergunta', 'perguntas')}
|
||||
${footerHtml(`<strong>Não debata para vencer.</strong> Preserve a relação, responda o verificável e anote a lacuna.`, 3)}
|
||||
const sheetEssential = (content, { name, noun }) => `
|
||||
<article class="sheet" data-page="essencial" aria-label="O essencial de ${htmlEscape(name)} e o pedido">
|
||||
${headerHtml(noun, 'O essencial do recorte', `${name} · fatos que sustentam a conversa`, 2)}
|
||||
<section class="section">
|
||||
<p class="eyebrow">Fatos-âncora · fase junto ao valor</p>
|
||||
<ul class="line-list">
|
||||
${lineItemsHtml(content.essential)}
|
||||
</ul>
|
||||
</section>
|
||||
<section class="request-block" aria-label="Pedido de voto">
|
||||
<p class="eyebrow">O pedido</p>
|
||||
<p class="request-line">${BRIEFING_REQUEST_LINE}</p>
|
||||
<p class="request-plan"><strong>Plano:</strong> ${htmlEscape(content.plan)}</p>
|
||||
</section>
|
||||
${footerHtml(noun, name, 2)}
|
||||
</article>`
|
||||
|
||||
const sheetAvoid = (content, { unit, report }) => `
|
||||
<article class="sheet sheet-compact" data-page="evitar" aria-label="O que evitar e o que conferir no dossiê">
|
||||
${headerHtml(unit, 'O que evitar', 'Proteja a relação, a verdade do recorte e os limites do material', 4, briefingSubjectName(report, content))}
|
||||
<section class="section">
|
||||
<ul class="avoid-list">
|
||||
${content.avoid
|
||||
.map((item) => `<li><strong>${htmlEscape(item.title)}</strong> ${htmlEscape(item.note)}</li>`)
|
||||
.join('\n')}
|
||||
</ul>
|
||||
${moreNote(content.shed.avoid, 'anti-padrão', 'anti-padrões')}
|
||||
</section>
|
||||
<div class="two-column">
|
||||
<section>
|
||||
<p class="eyebrow">Antes de responder</p>
|
||||
<h3 class="section-title">O que conferir no dossiê</h3>
|
||||
<ul class="check-list">
|
||||
${content.checklist.beforeAnswer.map((item) => `<li>${inlineBold(item)}</li>`).join('\n')}
|
||||
</ul>
|
||||
</section>
|
||||
<section>
|
||||
<p class="eyebrow">Consulta em campo</p>
|
||||
<h3 class="section-title">Se você não souber</h3>
|
||||
<ul class="check-list">
|
||||
${content.checklist.unsure.map((item) => `<li>${inlineBold(item)}</li>`).join('\n')}
|
||||
</ul>
|
||||
</section>
|
||||
</div>
|
||||
${moreNote(content.shed.checklist, 'item de conferência', 'itens de conferência')}
|
||||
<section class="limits">
|
||||
<p class="eyebrow">Limites e defeso</p>
|
||||
<p>${htmlEscape(limitsCopyA(briefingSubjectName(report, content)))}</p>
|
||||
<p>${htmlEscape(LIMITS_COPY_B)}</p>
|
||||
</section>
|
||||
${footerHtml('Anti-padrões: Lau et al. 2007 · Ecker et al. 2022 · Debunking Handbook 2020', 4)}
|
||||
const sheetQaFirst = ({ name, noun, items }) => `
|
||||
<article class="sheet" data-page="qa" aria-label="Perguntas prováveis e melhores respostas sobre ${htmlEscape(name)}, parte 1">
|
||||
${headerHtml(name, 'Perguntas prováveis × melhores respostas', 'Parte 1 de 2 · posições e escolhas', 3)}
|
||||
<div class="rule-note"><strong>Régua:</strong> ${htmlEscape(RULER_COPY)}</div>
|
||||
<ol class="qa-list">
|
||||
${qaItemsHtml(items)}
|
||||
</ol>
|
||||
${footerHtml(noun, name, 3)}
|
||||
</article>`
|
||||
|
||||
const sheetQaSecond = ({ name, noun, items, shed }) => `
|
||||
<article class="sheet" data-page="qa-2" aria-label="Perguntas prováveis e melhores respostas sobre ${htmlEscape(name)}, continuação">
|
||||
${headerHtml(name, 'Perguntas prováveis × melhores respostas', 'Continuação · parte 2 de 2 · entrega e atribuição', 4)}
|
||||
<ol class="qa-list">
|
||||
${qaItemsHtml(items)}
|
||||
</ol>
|
||||
${moreNote(shed, 'pergunta', 'perguntas')}
|
||||
${footerHtml(noun, name, 4)}
|
||||
</article>`
|
||||
|
||||
/**
|
||||
|
|
@ -279,15 +214,20 @@ ${content.checklist.unsure.map((item) => `<li>${inlineBold(item)}</li>`).join('\
|
|||
*/
|
||||
export const renderBriefingHtml = (content, { unit, report }) => {
|
||||
const resolvedUnit = resolveDossierUnit(unit ?? report?.unit)
|
||||
const name = briefingSubjectName(report, content)
|
||||
const noun = resolvedUnit.briefingNoun ?? resolvedUnit.id
|
||||
// The Q&A flows across the two last sheets: the first half prints on folha 3,
|
||||
// the second half on folha 4 (the shed re-splits when an item is dropped).
|
||||
const half = Math.ceil(content.qa.length / 2)
|
||||
const parts = [
|
||||
sheetEssential(content, { unit: resolvedUnit, report }),
|
||||
sheetDefenses(content, { unit: resolvedUnit, report }),
|
||||
sheetQa(content, { unit: resolvedUnit, report }),
|
||||
sheetAvoid(content, { unit: resolvedUnit, report }),
|
||||
sheetDefenses(content, { name, noun }),
|
||||
sheetEssential(content, { name, noun }),
|
||||
sheetQaFirst({ name, noun, items: content.qa.slice(0, half) }),
|
||||
sheetQaSecond({ name, noun, items: content.qa.slice(half), shed: content.shed.qa }),
|
||||
]
|
||||
return `<!doctype html>
|
||||
<html lang="pt-BR">
|
||||
<head><meta charset="utf-8"><title>Briefing de capacitação — ${htmlEscape(briefingSubjectName(report, content))}</title><style>${BRIEFING_PRINT_CSS}</style></head>
|
||||
<head><meta charset="utf-8"><title>Briefing de capacitação — ${htmlEscape(name)}</title><style>${BRIEFING_PRINT_CSS}</style></head>
|
||||
<body>
|
||||
${parts.join('\n')}
|
||||
</body>
|
||||
|
|
@ -309,31 +249,27 @@ export const renderBriefingMd = (content, { unit, report }) => {
|
|||
if (content.subtitle) lines.push(`- **Uso:** ${content.subtitle}`)
|
||||
lines.push(
|
||||
'',
|
||||
'## O essencial do recorte',
|
||||
'## O que Solla defende',
|
||||
'',
|
||||
content.lede,
|
||||
'',
|
||||
`**Como usar.** ${HOW_TO_USE_COPY}`,
|
||||
'',
|
||||
...content.essential.map((item) => `- **${item.title}** — ${item.note} (${anchorMd(item)})`),
|
||||
'',
|
||||
'## O que Solla defende',
|
||||
'',
|
||||
...(content.defenses.length > 0
|
||||
? content.defenses.map((item) => `- **${item.title}** — ${item.note} (${anchorMd(item)})`)
|
||||
: ['- Sem registro localizado no dossiê — não completar por memória.']),
|
||||
'',
|
||||
'## Faça o pedido com clareza',
|
||||
'## O essencial do recorte',
|
||||
'',
|
||||
...content.essential.map((item) => `- **${item.title}** — ${item.note} (${anchorMd(item)})`),
|
||||
'',
|
||||
'## O pedido',
|
||||
'',
|
||||
'> “Posso contar com você? Para deputado federal, vote 1313, Jorge Solla.”',
|
||||
'',
|
||||
`**Por que assim:** ${WHY_COPY}`,
|
||||
'',
|
||||
...content.script.steps.map((step, index) => `${index + 1}. **${step.title}** — ${step.note}`),
|
||||
`**Plano:** ${content.plan}`,
|
||||
'',
|
||||
'## Perguntas prováveis × melhores respostas',
|
||||
'',
|
||||
`**Régua visível:** ${RULER_COPY}`,
|
||||
`**Régua:** ${RULER_COPY}`,
|
||||
'',
|
||||
...content.qa.flatMap((item) => [
|
||||
`### ${QA_SIDE_LABELS[item.side] ?? item.side} — ${item.question}`,
|
||||
|
|
@ -343,24 +279,6 @@ export const renderBriefingMd = (content, { unit, report }) => {
|
|||
`- **Fechar:** ${item.close}`,
|
||||
'',
|
||||
]),
|
||||
'## O que evitar',
|
||||
'',
|
||||
...content.avoid.map((item) => `- **${item.title}** ${item.note}`),
|
||||
'',
|
||||
'## O que conferir no dossiê',
|
||||
'',
|
||||
...content.checklist.beforeAnswer.map((item) => `- ${item}`),
|
||||
'',
|
||||
'## Se você não souber',
|
||||
'',
|
||||
...content.checklist.unsure.map((item) => `- ${item}`),
|
||||
'',
|
||||
'## Limites e defeso',
|
||||
'',
|
||||
limitsCopyA(name),
|
||||
'',
|
||||
LIMITS_COPY_B,
|
||||
'',
|
||||
`_Documento interno de campanha, datado (${formatDateTimeBr(content.generatedAt)}). O dado envelhece: confira a data do dossiê._`,
|
||||
)
|
||||
return `${lines.join('\n')}\n`
|
||||
|
|
|
|||
|
|
@ -15,9 +15,11 @@ import {
|
|||
} from '../../scripts/lib/dossieResearch.mjs'
|
||||
import { MUNICIPALITY_UNIT, THEME_UNIT } from '../../scripts/lib/dossieUnit.mjs'
|
||||
|
||||
// C210: the authored briefing may only anchor on the dossiê ledger (facts with
|
||||
// source). These tests pin the fail-closed validation, the deny-list and the
|
||||
// deterministic shed order that keeps the four-sheet PDF honest.
|
||||
// C210 (replanejado 2026-09-23): the authored briefing may only anchor on the
|
||||
// dossiê ledger (facts with source) and is 100% recorte — princípios/crenças,
|
||||
// defesas, fatos-âncora, uma linha de plano e Q&A. These tests pin the
|
||||
// fail-closed validation, the deny-list and the deterministic shed order that
|
||||
// keeps the four-sheet PDF honest.
|
||||
|
||||
const generatedAt = new Date('2026-09-22T12:00:00.000Z')
|
||||
const snapshot = {
|
||||
|
|
@ -73,20 +75,17 @@ const validRaw = (overrides: Record<string, unknown> = {}) => ({
|
|||
municipalitySlug: 'ilheus',
|
||||
generatedAt: generatedAt.toISOString(),
|
||||
subtitle: 'Litoral Sul · consulta antes e durante o contato',
|
||||
lede: 'Use um fato local de cada vez.',
|
||||
lede: 'Universidade pública não é gasto: é projeto de país.',
|
||||
defenses: [
|
||||
{ factId: 'era_c_atuacao', title: 'Ensino superior', note: 'Defesa regional.' },
|
||||
{ factId: 'era_b_programas', title: 'Saúde da Família', note: 'Defesa local.' },
|
||||
],
|
||||
essential: [
|
||||
{ factId: 'era_b_equipamentos', title: 'Ambulância', note: 'Registro local de 2012.' },
|
||||
{ factId: 'era_b_programas', title: 'Saúde em Movimento', note: 'Menção direta.' },
|
||||
{ gapReason: 'sem fala própria localizada', title: 'Sem fala local', note: 'Anote a lacuna.' },
|
||||
],
|
||||
defenses: [{ factId: 'era_c_atuacao', title: 'Ensino superior', note: 'Defesa regional.' }],
|
||||
script: {
|
||||
steps: [
|
||||
{ title: 'Comece pela relação', note: 'Escute antes de argumentar.' },
|
||||
{ title: 'Use um fato do recorte', note: 'Diga o alcance.' },
|
||||
{ title: 'Peça explicitamente', note: 'Nome e número.' },
|
||||
],
|
||||
},
|
||||
plan: 'Combine onde, quando e como votar e registre o compromisso nomeado.',
|
||||
qa: [
|
||||
{
|
||||
side: 'direita',
|
||||
|
|
@ -129,17 +128,6 @@ const validRaw = (overrides: Record<string, unknown> = {}) => ({
|
|||
factId: 'era_c_atuacao',
|
||||
},
|
||||
],
|
||||
avoid: [
|
||||
{ title: 'Confrontar para ganhar', note: 'Encerre com respeito.' },
|
||||
{ title: 'Envergonhar o eleitor', note: 'Pressão pesada gera reação.' },
|
||||
{ title: 'Prometer sem lastro', note: 'Indicação não é obra.' },
|
||||
{ title: 'Repetir o ataque', note: 'Ofereça a explicação alternativa.' },
|
||||
{ title: 'Broadcast impessoal', note: 'WhatsApp 1-a-1 vem antes.' },
|
||||
],
|
||||
checklist: {
|
||||
beforeAnswer: ['Fonte e data', 'Alcance', 'Fase do valor'],
|
||||
unsure: ['Diga que não tem o dado', 'Anote a pergunta', 'Combine retorno'],
|
||||
},
|
||||
...overrides,
|
||||
})
|
||||
|
||||
|
|
@ -152,7 +140,10 @@ describe('normalizeBriefingContent', () => {
|
|||
expect(content.essential).toHaveLength(3)
|
||||
expect(content.essential[0].fact?.sourceUrl).toBe('https://saude.test/ambulancia')
|
||||
expect(content.essential[2].gapReason).toBe('sem fala própria localizada')
|
||||
expect(content.defenses).toHaveLength(2)
|
||||
expect(content.defenses[0].fact?.sourceUrl).toBe('https://camara.test/ufba')
|
||||
expect(content.qa).toHaveLength(5)
|
||||
expect(content.plan).toMatch(/compromisso nomeado/)
|
||||
expect(content.warnings).toEqual([])
|
||||
})
|
||||
|
||||
|
|
@ -161,9 +152,10 @@ describe('normalizeBriefingContent', () => {
|
|||
expect(() => normalize(validRaw({ municipalitySlug: 'itabuna' }))).toThrow(/não é "ilheus"/)
|
||||
})
|
||||
|
||||
it('requires generatedAt and a lede', () => {
|
||||
it('requires generatedAt, a lede and the one-line plan', () => {
|
||||
expect(() => normalize(validRaw({ generatedAt: 'ontem' }))).toThrow(/generatedAt inválido/)
|
||||
expect(() => normalize(validRaw({ lede: '' }))).toThrow(/"lede" obrigatório/)
|
||||
expect(() => normalize(validRaw({ plan: '' }))).toThrow(/"plan" obrigatório/)
|
||||
})
|
||||
|
||||
it('demands exactly one of factId | gapReason per item', () => {
|
||||
|
|
@ -218,16 +210,15 @@ describe('normalizeBriefingContent', () => {
|
|||
),
|
||||
}),
|
||||
).toThrow(/dois lados/)
|
||||
expect(() => normalize({ ...raw, avoid: [] })).toThrow(/avoid: mínimo de 3/)
|
||||
expect(() =>
|
||||
normalize({ ...raw, checklist: { beforeAnswer: ['só um'], unsure: ['a', 'b'] } }),
|
||||
).toThrow(/checklist.beforeAnswer: mínimo de 2/)
|
||||
expect(() => normalize({ ...raw, qa: [] })).toThrow(/qa: mínimo de 4/)
|
||||
expect(() => normalize({ ...raw, essential: [] })).toThrow(/essential: mínimo de 3/)
|
||||
})
|
||||
|
||||
it('reports over-cap text as a warning, never as a hard failure', () => {
|
||||
const raw = validRaw()
|
||||
const content = normalize({ ...raw, lede: 'a'.repeat(500) })
|
||||
const content = normalize({ ...raw, lede: 'a'.repeat(500), plan: 'b'.repeat(300) })
|
||||
expect(content.warnings.join('\n')).toMatch(/lede: 500 chars/)
|
||||
expect(content.warnings.join('\n')).toMatch(/plan: 300 chars/)
|
||||
})
|
||||
|
||||
it('resolves anchors through the unit slugField (theme seam)', () => {
|
||||
|
|
@ -267,6 +258,48 @@ describe('normalizeBriefingContent', () => {
|
|||
).toThrow(/não resolve em fato com fonte/)
|
||||
})
|
||||
|
||||
it('resolves a repeated id through the item sourceUrl and fails closed on a wrong pair', () => {
|
||||
const duplicated = [
|
||||
{
|
||||
id: 'era_b_equipamentos',
|
||||
sourceUrl: 'https://saude.test/ambulancia-2',
|
||||
sourceDate: '2019-01-01',
|
||||
},
|
||||
...facts,
|
||||
]
|
||||
const raw = validRaw({
|
||||
essential: [
|
||||
{
|
||||
factId: 'era_b_equipamentos',
|
||||
sourceUrl: 'https://saude.test/ambulancia-2',
|
||||
title: 'Ambulância nova',
|
||||
note: 'Registro de 2019.',
|
||||
},
|
||||
{ factId: 'era_b_programas', title: 'Saúde em Movimento', note: 'Menção direta.' },
|
||||
{
|
||||
gapReason: 'sem fala própria localizada',
|
||||
title: 'Sem fala local',
|
||||
note: 'Anote a lacuna.',
|
||||
},
|
||||
],
|
||||
})
|
||||
const content = normalizeBriefingContent(raw, {
|
||||
unit: MUNICIPALITY_UNIT,
|
||||
slug: 'ilheus',
|
||||
facts: duplicated,
|
||||
})
|
||||
expect(content.essential[0].fact?.sourceDate).toBe('2019-01-01')
|
||||
const wrongPair = (raw.essential as Record<string, unknown>[]).map((item, index) =>
|
||||
index === 0 ? { ...item, sourceUrl: 'https://saude.test/nao-existe' } : item,
|
||||
)
|
||||
expect(() =>
|
||||
normalizeBriefingContent(
|
||||
{ ...raw, essential: wrongPair },
|
||||
{ unit: MUNICIPALITY_UNIT, slug: 'ilheus', facts: duplicated },
|
||||
),
|
||||
).toThrow(/não resolve em fato com fonte/)
|
||||
})
|
||||
|
||||
it('reports qa caps with the real field names, once each', () => {
|
||||
const raw = validRaw()
|
||||
const qa = (raw.qa as Record<string, unknown>[]).map((item, index) =>
|
||||
|
|
@ -291,28 +324,43 @@ describe('briefingAnchorFact', () => {
|
|||
)
|
||||
expect(briefingAnchorFact(facts, 'não-existe')).toBeNull()
|
||||
})
|
||||
|
||||
it('disambiguates a repeated id by the exact sourceUrl', () => {
|
||||
const duplicated = [
|
||||
{
|
||||
id: 'era_b_programas',
|
||||
sourceUrl: 'https://saude.test/programas',
|
||||
sourceDate: '2013-05-10',
|
||||
},
|
||||
{
|
||||
id: 'era_b_programas',
|
||||
sourceUrl: 'https://saude.test/programas-2',
|
||||
sourceDate: '2020-01-01',
|
||||
},
|
||||
]
|
||||
expect(briefingAnchorFact(duplicated, 'era_b_programas')?.sourceDate).toBe('2013-05-10')
|
||||
expect(
|
||||
briefingAnchorFact(duplicated, 'era_b_programas', 'https://saude.test/programas-2')
|
||||
?.sourceDate,
|
||||
).toBe('2020-01-01')
|
||||
expect(briefingAnchorFact(duplicated, 'era_b_programas', 'https://nope.test')).toBeNull()
|
||||
})
|
||||
})
|
||||
|
||||
describe('trimBriefing (deterministic shed order)', () => {
|
||||
const content = normalize()
|
||||
|
||||
it('drops qa first, then defenses, then checklist, then avoid', () => {
|
||||
it('drops qa first, then defenses, then stops', () => {
|
||||
const first = trimBriefing(content)!
|
||||
expect(first.qa).toHaveLength(BRIEFING_MINIMUMS.qa)
|
||||
expect(first.shed).toEqual({ qa: 1, defenses: 0, checklist: 0, avoid: 0 })
|
||||
expect(first.shed).toEqual({ qa: 1, defenses: 0 })
|
||||
const second = trimBriefing(first)!
|
||||
expect(second.defenses).toHaveLength(0)
|
||||
expect(second.shed).toEqual({ qa: 1, defenses: 1, checklist: 0, avoid: 0 })
|
||||
expect(second.defenses).toHaveLength(1)
|
||||
expect(second.shed).toEqual({ qa: 1, defenses: 1 })
|
||||
const third = trimBriefing(second)!
|
||||
expect(third.shed.checklist).toBe(1)
|
||||
const fourth = trimBriefing(third)!
|
||||
expect(fourth.shed.checklist).toBe(2)
|
||||
expect(fourth.checklist.beforeAnswer.length + fourth.checklist.unsure.length).toBe(
|
||||
BRIEFING_MINIMUMS.beforeAnswer + BRIEFING_MINIMUMS.unsure,
|
||||
)
|
||||
const fifth = trimBriefing(fourth)!
|
||||
expect(fifth.avoid).toHaveLength(BRIEFING_MINIMUMS.avoid + 1)
|
||||
expect(fifth.shed.avoid).toBe(1)
|
||||
expect(third.defenses).toHaveLength(0)
|
||||
expect(third.shed).toEqual({ qa: 1, defenses: 2 })
|
||||
expect(trimBriefing(third)).toBeNull()
|
||||
})
|
||||
|
||||
it('never silences a required side when shedding qa', () => {
|
||||
|
|
@ -334,7 +382,7 @@ describe('trimBriefing (deterministic shed order)', () => {
|
|||
expect(trimmed.qa.at(-1)?.side).toBe('direita')
|
||||
})
|
||||
|
||||
it('never touches essential or the script and stops at the minimums', () => {
|
||||
it('never touches the essential, the principles copy or the plan', () => {
|
||||
let current = content
|
||||
let next = trimBriefing(current)
|
||||
let guard = 0
|
||||
|
|
@ -345,11 +393,9 @@ describe('trimBriefing (deterministic shed order)', () => {
|
|||
}
|
||||
expect(current.qa).toHaveLength(BRIEFING_MINIMUMS.qa)
|
||||
expect(current.defenses).toHaveLength(0)
|
||||
expect(current.checklist.beforeAnswer).toHaveLength(BRIEFING_MINIMUMS.beforeAnswer)
|
||||
expect(current.checklist.unsure).toHaveLength(BRIEFING_MINIMUMS.unsure)
|
||||
expect(current.avoid).toHaveLength(BRIEFING_MINIMUMS.avoid)
|
||||
expect(current.essential).toEqual(content.essential)
|
||||
expect(current.script).toEqual(content.script)
|
||||
expect(current.lede).toBe(content.lede)
|
||||
expect(current.plan).toBe(content.plan)
|
||||
expect(next).toBeNull()
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -9,8 +9,9 @@ import {
|
|||
} from '../../scripts/lib/dossieResearch.mjs'
|
||||
import { MUNICIPALITY_UNIT } from '../../scripts/lib/dossieUnit.mjs'
|
||||
|
||||
// C210: the briefing is the approved gate design ported to inline print CSS —
|
||||
// four fixed A4 sheets, the internal label on each and the request literal. The
|
||||
// C210 (replanejado 2026-09-23): the briefing is the approved gate design ported
|
||||
// to inline print CSS — four fixed recorte sheets (defesas → essencial + pedido
|
||||
// → qa 1/2 → qa 2/2), the internal label on each and the request literal. The
|
||||
// tests pin the anchors the page guard reads, the guards that cannot regress
|
||||
// (literal label, "vote 1313", source per anchor) and the `.md` superset.
|
||||
|
||||
|
|
@ -47,12 +48,16 @@ const research = mergeDossierResearch([
|
|||
const report = buildDossierReport({ snapshot, research, generatedAt })
|
||||
const facts = report.bulletinFacts
|
||||
|
||||
const rawContent = (qaCount = 5, avoidCount = 5) => ({
|
||||
const rawContent = (qaCount = 5) => ({
|
||||
unitId: 'municipality',
|
||||
municipalitySlug: 'ilheus',
|
||||
generatedAt: generatedAt.toISOString(),
|
||||
subtitle: 'Litoral Sul · consulta antes e durante o contato',
|
||||
lede: 'Use um fato local de cada vez.',
|
||||
lede: 'Universidade pública não é gasto: é projeto de país.',
|
||||
defenses: [
|
||||
{ factId: 'era_b_programas', title: 'Ensino superior', note: 'Defesa regional.' },
|
||||
{ factId: 'era_b_equipamentos', title: 'Saúde perto de casa', note: 'Defesa local.' },
|
||||
],
|
||||
essential: [
|
||||
{
|
||||
factId: 'era_b_equipamentos',
|
||||
|
|
@ -62,14 +67,7 @@ const rawContent = (qaCount = 5, avoidCount = 5) => ({
|
|||
{ factId: 'era_b_programas', title: 'Saúde em Movimento', note: 'Menção direta.' },
|
||||
{ gapReason: 'sem fala própria localizada', title: 'Sem fala local', note: 'Anote a lacuna.' },
|
||||
],
|
||||
defenses: [{ factId: 'era_b_programas', title: 'Ensino superior', note: 'Defesa regional.' }],
|
||||
script: {
|
||||
steps: [
|
||||
{ title: 'Comece pela relação', note: 'Escute antes de argumentar.' },
|
||||
{ title: 'Use um fato do recorte', note: 'Diga o alcance.' },
|
||||
{ title: 'Peça explicitamente', note: 'Nome e número.' },
|
||||
],
|
||||
},
|
||||
plan: 'Combine onde, quando e como votar e registre o compromisso nomeado.',
|
||||
qa: Array.from({ length: qaCount }, (_value, index) => ({
|
||||
side: index % 2 === 0 ? 'direita' : 'esquerda',
|
||||
question: `Pergunta ${index}`,
|
||||
|
|
@ -78,14 +76,6 @@ const rawContent = (qaCount = 5, avoidCount = 5) => ({
|
|||
close: 'Posso contar com o 1313?',
|
||||
factId: 'era_b_equipamentos',
|
||||
})),
|
||||
avoid: Array.from({ length: avoidCount }, (_value, index) => ({
|
||||
title: `Anti-padrão ${index}`,
|
||||
note: 'Encerre com respeito.',
|
||||
})),
|
||||
checklist: {
|
||||
beforeAnswer: ['**Fonte e data** do fato', '**Alcance:** município'],
|
||||
unsure: ['**Diga com clareza:** não tenho', '**Anote a pergunta** sem PII'],
|
||||
},
|
||||
})
|
||||
|
||||
const content = normalizeBriefingContent(rawContent(), {
|
||||
|
|
@ -97,8 +87,8 @@ const content = normalizeBriefingContent(rawContent(), {
|
|||
describe('renderBriefingHtml', () => {
|
||||
const html = renderBriefingHtml(content, { unit: MUNICIPALITY_UNIT, report })
|
||||
|
||||
it('renders exactly the four fixed sheets the page guard counts', () => {
|
||||
for (const anchor of ['essencial', 'defesas', 'qa', 'evitar']) {
|
||||
it('renders exactly the four fixed recorte sheets the page guard counts', () => {
|
||||
for (const anchor of ['defesas', 'essencial', 'qa', 'qa-2']) {
|
||||
expect(html).toContain(`data-page="${anchor}"`)
|
||||
}
|
||||
expect((html.match(/class="sheet/g) ?? []).length).toBe(4)
|
||||
|
|
@ -111,9 +101,30 @@ describe('renderBriefingHtml', () => {
|
|||
}
|
||||
})
|
||||
|
||||
it('renders the literal request with the vote number', () => {
|
||||
it('opens with the principles copy and the defenses, not a cover', () => {
|
||||
const firstSheet = html.split('data-page="essencial"')[0]
|
||||
expect(firstSheet).toContain('Princípios e crenças')
|
||||
expect(firstSheet).toContain('Universidade pública não é gasto: é projeto de país.')
|
||||
expect(firstSheet).toContain('O que Solla defende')
|
||||
expect(firstSheet).not.toContain('Como usar')
|
||||
})
|
||||
|
||||
it('renders the literal request with the vote number and the one-line plan', () => {
|
||||
expect(html).toContain('vote 1313, Jorge Solla')
|
||||
expect(html).toContain('request-line')
|
||||
expect(html).toContain('request-block')
|
||||
expect(html).toContain('compromisso nomeado')
|
||||
})
|
||||
|
||||
it('splits the qa list across the two last sheets', () => {
|
||||
const firstQa = html.split('data-page="qa"')[1].split('data-page="qa-2"')[0]
|
||||
const secondQa = html.split('data-page="qa-2"')[1]
|
||||
expect((html.match(/class="qa-item"/g) ?? []).length).toBe(5)
|
||||
expect(firstQa).toContain('Pergunta 0')
|
||||
expect(firstQa).toContain('Pergunta 2')
|
||||
expect(firstQa).not.toContain('Pergunta 4')
|
||||
expect(secondQa).toContain('Pergunta 3')
|
||||
expect(secondQa).toContain('Pergunta 4')
|
||||
})
|
||||
|
||||
it('links every anchored item to its sourced fact and marks the lacuna', () => {
|
||||
|
|
@ -134,14 +145,6 @@ describe('renderBriefingHtml', () => {
|
|||
expect(trimmed.qa).toHaveLength(4)
|
||||
})
|
||||
|
||||
it('does not contradict the shed defenses with the no-record lacuna line', () => {
|
||||
const trimmed = trimBriefing(trimBriefing(content)!)!
|
||||
const shedHtml = renderBriefingHtml(trimmed, { unit: MUNICIPALITY_UNIT, report })
|
||||
expect(trimmed.defenses).toHaveLength(0)
|
||||
expect(shedHtml).toContain('e mais 1 defesa no briefing completo (.md)')
|
||||
expect(shedHtml).not.toContain('Sem registro localizado no dossiê.')
|
||||
})
|
||||
|
||||
it('prints the lacuna line only when the recorte never had a defense', () => {
|
||||
const noDefenses = normalizeBriefingContent(
|
||||
{ ...rawContent(), defenses: [] },
|
||||
|
|
@ -152,16 +155,10 @@ describe('renderBriefingHtml', () => {
|
|||
)
|
||||
})
|
||||
|
||||
it('keeps the guard copy of the unit on the first and last sheets', () => {
|
||||
expect(html).toContain('região e polo não são somados ao município')
|
||||
expect(html).toContain('Não publicar, encaminhar como peça')
|
||||
})
|
||||
|
||||
it('bold-prints the checklist scan prefix and names the recorte in the kicker', () => {
|
||||
expect(html).toContain('<strong>Fonte e data</strong> do fato')
|
||||
expect(html).toContain('<strong>Diga com clareza:</strong> não tenho')
|
||||
it('names the recorte in the kicker and the footer', () => {
|
||||
expect(html).toContain('Briefing de capacitação · Ilhéus')
|
||||
expect(html).toContain('Briefing de capacitação · cidade')
|
||||
expect(html).toContain('<strong>Recorte:</strong> cidade · Ilhéus')
|
||||
})
|
||||
})
|
||||
|
||||
|
|
@ -170,7 +167,7 @@ describe('renderBriefingMd (companion superset)', () => {
|
|||
let trimmed = content
|
||||
while (trimBriefing(trimmed)) trimmed = trimBriefing(trimmed)!
|
||||
expect(trimmed.qa.length).toBeLessThan(content.qa.length)
|
||||
expect(trimmed.avoid.length).toBeLessThan(content.avoid.length)
|
||||
expect(trimmed.defenses.length).toBeLessThan(content.defenses.length)
|
||||
const droppedQuestion = content.qa.find(
|
||||
(item: { question: string }) =>
|
||||
!trimmed.qa.some((kept: { question: string }) => kept.question === item.question),
|
||||
|
|
@ -181,13 +178,14 @@ describe('renderBriefingMd (companion superset)', () => {
|
|||
expect(md).toContain(droppedQuestion.question)
|
||||
expect(md).toContain('Insumo interno de capacitação — não publicar')
|
||||
expect(md).toContain('vote 1313, Jorge Solla')
|
||||
expect(md).toContain('**Plano:** Combine onde, quando e como votar')
|
||||
expect(md).toContain('[fonte](https://saude.test/ambulancia)')
|
||||
expect(md).toContain('## O que conferir no dossiê')
|
||||
expect(md).toContain('## Limites e defeso')
|
||||
// The fixed guidance printed on the sheets is part of the reference copy.
|
||||
expect(md).toContain('**Como usar.**')
|
||||
expect(md).toContain('**Por que assim:**')
|
||||
expect(md).toContain('**Régua visível:**')
|
||||
expect(md).toContain('## O que Solla defende')
|
||||
expect(md).toContain('## O essencial do recorte')
|
||||
expect(md).toContain('## O pedido')
|
||||
expect(md).toContain('## Perguntas prováveis × melhores respostas')
|
||||
expect(md).not.toContain('## O que evitar')
|
||||
expect(md).not.toContain('## Limites e defeso')
|
||||
expect((md.match(/^### /gm) ?? []).length).toBe(5)
|
||||
})
|
||||
})
|
||||
|
|
|
|||
Loading…
Reference in a new issue