/**
* Dossiê renderer (C186): the block model goes to print HTML (Chromium
* `page.pdf`, A4, one `.sheet` per page) and to the companion `.md`. Ports the
* approved hi-fi classes (`dossie-solla-cidade-ui-design.html`) class-by-class;
* shared escaping/source-token contract lives in `reportText.mjs`.
*/
import { formatDateBr, formatDateTimeBr } from './cityReportFormat.mjs'
import { dossierPhaseLabel, dossierSphereLabel } from './dossieBlocks.mjs'
import { htmlEscape, stripInlineSources } from './reportText.mjs'
const SOURCE_ICON =
' '
const WARNING_ICON =
' '
const sourceLink = (url, label = '(fonte)') =>
url
? `${SOURCE_ICON}${htmlEscape(label)} `
: ''
const scopeBadge = (sphere, label = null) =>
`${htmlEscape(label ?? dossierSphereLabel(sphere))} `
/** `dd/mm/aa` — the news table column is 9% wide (no room for a 4-digit year). */
const formatShortDateBr = (value) => {
const full = formatDateBr(value)
const parts = full.split('/')
return parts.length === 3 ? `${parts[0]}/${parts[1]}/${parts[2].slice(-2)}` : full
}
/** Research text without the inline `{{fonte}}` markers — the row's own link carries the source. */
const cleanText = (value) => htmlEscape(stripInlineSources(value))
const phaseBadge = (phase) => `${htmlEscape(dossierPhaseLabel(phase))} `
const placeholder = (label) =>
` `
const valueCell = (value, widthLabel = 'Valor desconhecido') =>
value === null || value === undefined || value === ''
? placeholder(widthLabel)
: `${htmlEscape(value)} `
const assetBox = (title, lines) =>
`
${htmlEscape(title)}
${lines
.map((line) => htmlEscape(line))
.join(' ')}
`
/** cover + trajetória + resumo + região + fontes (eras and gap sheets added separately). */
const SHEET_COUNT_BASE = 5
const pageCount = (report) => report.meta.pageTotal
/** Gaps are paginated, never truncated: 12 rows per A4 sheet. */
const GAPS_PER_PAGE = 12
const chunkGaps = (gaps) => {
const list = Array.isArray(gaps) ? gaps : []
if (list.length === 0) return [[]]
const chunks = []
for (let index = 0; index < list.length; index += GAPS_PER_PAGE) {
chunks.push(list.slice(index, index + GAPS_PER_PAGE))
}
return chunks
}
const timelineGrid = (rows) => `
${rows
.map(
(row) => `
${htmlEscape(row.period)}
${htmlEscape(row.role)}
${htmlEscape(row.recovery)}${row.uncertain ? ` · ${htmlEscape(row.uncertain)}` : ''}
`,
)
.join('')}
`
const sheetHeader = (report, title, subtitle, pageNo) => `
`
const sheetFooter = (report, left, pageNo) => `
`
const renderCover = (report) => `
${htmlEscape(report.cover.kicker)}
${htmlEscape(report.cover.series)}
${htmlEscape(report.cover.internalNote)} ${htmlEscape(report.cover.internalNoteSub)}
${htmlEscape(report.cover.eyebrow)}
${htmlEscape(report.cover.title)}
${htmlEscape(report.cover.subtitle)}
Território de Identidade ${htmlEscape(report.cover.territory)}
Região / polo adotado ${htmlEscape(report.cover.pole)}
Data de geração ${htmlEscape(formatDateTimeBr(report.meta.generatedAt))}
Como usar
${htmlEscape(report.cover.howToUse)}
${assetBox('NEEDS ASSET', ['selo institucional autorizado', 'opcional — nunca marca de campanha'])}
`
const renderTimeline = (report) => `
01 · trajetória
Linha do tempo da carreira
Período + papel + trilha de recuperação
${timelineGrid(report.page1.timeline)}
`
const renderTrajectory = (report, pageNo) => `
${sheetHeader(report, 'Trajetória completa', 'Todos os períodos da carreira e como cada um foi recuperado', pageNo)}
${timelineGrid(report.trajectory)}
Períodos incertos ficam marcados na própria linha; o acervo interno de falas cobre 2011+ e o que falta vira lacuna explícita.
${sheetFooter(report, 'Trajetória', pageNo)}
`
const renderDeliveries = (report) => {
if (report.page1.deliveries.length === 0) return ''
return `
02 · destaques com lastro
Principais entregas localizadas
${report.page1.deliveries
.map(
(row) => `
${scopeBadge(row.sphere)}
Era ${htmlEscape(row.era)}${row.year ? ` · ${htmlEscape(row.year)}` : ''}
${cleanText(row.title)}
${row.detail ? `
${cleanText(row.detail)}
` : ''}
${valueCell(row.value, 'Valor não informado')}
${row.phase ? phaseBadge(row.phase) : ''}
${sourceLink(row.sourceUrl)}
`,
)
.join('')}
Guarda de leitura: autorizado ≠ empenhado ≠ liquidado ≠ pago. A fase acompanha cada valor.
`
}
const renderHooksAndPending = (report) => {
const hooks = report.page1.hooks
const pending = report.page1.pending
if (hooks.length === 0 && pending.length === 0) return ''
return `
${
hooks.length
? `
03 · ganchos para o boletim
${hooks
.map(
(hook) =>
`Tema: ${htmlEscape(hook.topic)}. Ângulo: ${cleanText(hook.angle)} ${sourceLink(hook.sourceUrl)} `,
)
.join('')}
`
: '
'
}
${
pending.length
? `
04 · o que falta
${pending.map((item) => `${htmlEscape(item)} `).join('')}
Ausência é resultado: não completar por inferência.
`
: '
'
}
`
}
const renderSummary = (report, pageNo) => `
${sheetHeader(report, 'Resumo de uma olhada', `${report.meta.region} · informação datada e pronta para conferência`, pageNo)}
${renderTimeline(report)}
${renderDeliveries(report)}
${renderHooksAndPending(report)}
${sheetFooter(report, 'Resumo', pageNo)}
`
const renderEra = (report, era, pageNo) => `
${sheetHeader(report, era.label, `${era.period} · ${era.subtitle}`, pageNo)}
Recorte e método
${htmlEscape(era.method)}
Trilha de recuperação ${htmlEscape(era.recovery)}
${
era.numbers.length
? `
Números com fase
Recursos e entregas quantificáveis
Valores por ano, fase, esfera e fonte
Objeto Valor Ano Fase Esfera Fonte
${era.numbers
.map(
(row) => `
${htmlEscape(row.object)}
${valueCell(row.value, 'Valor a recuperar')}
${htmlEscape(row.year ?? '—')}
${phaseBadge(row.phase)}
${scopeBadge(row.sphere)}
${sourceLink(row.sourceUrl)}${row.sphere !== 'municipio' ? 'não somar à cidade ' : ''}
`,
)
.join('')}
Não consolidar fases como se fossem equivalentes. Um valor empenhado não é um valor pago.
`
: ''
}
${
era.actions.length
? `
Atuação registrada
O que fez — item, alcance e lastro
${era.actions
.map(
(action) => `
${scopeBadge(action.sphere)}${htmlEscape(action.year ?? '—')}
${cleanText(action.title)}
${action.detail ? `${cleanText(action.detail)}
` : ''}
${sourceLink(action.sourceUrl)}
`,
)
.join('')}
`
: ''
}
${
era.honors.length
? `
Títulos, honrarias e vínculos locais
${era.honors
.map((honor) => `${cleanText(honor.text)} ${sourceLink(honor.sourceUrl)} `)
.join('')}
${assetBox('NEEDS ASSET', ['reprodução do ato ou clipping', 'somente com origem e licença'])}
`
: ''
}
${sheetFooter(report, era.label, pageNo)}
`
const renderScopeLists = (report) => `
Leitura lado a lado
Duas listas, dois totais, nenhuma soma
${scopeBadge('municipio')}${report.region.municipal.total} ${report.region.municipal.total === 1 ? 'item' : 'itens'} com fonte
${report.region.municipal.items
.map((item) => `${cleanText(item.answer)} ${sourceLink(item.sourceUrl)} `)
.join(
'',
)}${report.region.municipal.items.length === 0 ? 'Nenhum item municipal com fonte. ' : ''}
≠
${scopeBadge('regiao', 'região / polo')}SEM TOTAL COMBINADO · ${report.region.regional.total} ${report.region.regional.total === 1 ? 'item' : 'itens'}
${report.region.regional.items
.map((item) => `${cleanText(item.answer)} ${sourceLink(item.sourceUrl)} `)
.join(
'',
)}${report.region.regional.items.length === 0 ? 'Nenhum item regional com fonte. ' : ''}
`
const renderRegion = (report, pageNo) => `
${sheetHeader(report, 'Região / polo', 'Impacto compartilhado, apresentado fora da conta municipal', pageNo)}
${WARNING_ICON}
${htmlEscape(report.region.ruleTitle)}
${htmlEscape(report.region.ruleBody)}
${
report.region.context.length
? `Contexto municipal (IBGE): ${report.region.context
.map(
(item) => `${htmlEscape(item.detail)} ${sourceLink(item.sourceUrl, `(${item.topic})`)}`,
)
.join(' · ')} — leitura relativa, nunca % estadual absoluto.
`
: ''
}
${renderScopeLists(report)}
${
report.region.items.length
? `
Itens regionais recuperados
Evidência de alcance, sem apropriação local ${scopeBadge('regiao')}
Itens do recorte regional com evidência e fonte
Item Esfera Evidência de alcance Fonte
${report.region.items
.map(
(item) => `
${cleanText(item.item)}
${scopeBadge(item.sphere)}
${cleanText(item.evidence)}
${sourceLink(item.sourceUrl)}
`,
)
.join('')}
`
: ''
}
${
report.region.hook || report.region.priorityGap
? `
${
report.region.hook
? `Gancho possível
Tema: ${htmlEscape(report.region.hook.topic)}. Ângulo: ${cleanText(report.region.hook.angle)} ${sourceLink(report.region.hook.sourceUrl)}
`
: '
'
}
${
report.region.priorityGap
? `Lacuna prioritária
${htmlEscape(report.region.priorityGap)}
`
: '
'
}
`
: ''
}
${sheetFooter(report, 'Região/polo · recortes não somáveis', pageNo)}
`
const renderGapsPage = (report, chunk, index, pageNo) => {
const rows = chunk.length
? chunk
.map(
(gap) => `
${htmlEscape(gap.label)}
${htmlEscape(gap.reason)}
Não completar por inferência.
${htmlEscape(gap.nextStep)}
`,
)
.join('')
: 'Nenhuma lacuna declarada — confira a cobertura das eras. '
return `
${sheetHeader(report, 'Lacunas explícitas', `O que não foi encontrado${index > 0 ? ` (continuação ${index + 1})` : ''}`, pageNo)}
O que não foi encontrado
Lacunas explícitas Não completar por memória ou inferência
Tabela de lacunas explícitas e próximos passos de apuração
Lacuna Situação da busca Limite editorial Próximo passo
${rows}
${sheetFooter(report, 'Lacunas explícitas · INSUMO INTERNO', pageNo)}
`
}
const renderSourcesPage = (report, pageNo) => `
${sheetHeader(report, 'Fontes e limites', 'O que foi consultado e o que não se pode afirmar', pageNo)}
${
report.news.length
? `
Lista clicável
Notícias e documentos consultados
Fontes consultadas com largura fixa: data 12%, veículo 13%, título 47% e link 28%
Data Veículo Título Link
${report.news
.map(
(row) => `
${htmlEscape(formatShortDateBr(row.date))}
${htmlEscape(row.outlet)}
${htmlEscape(row.title)}
${sourceLink(row.url, '(fonte)')}
`,
)
.join('')}
`
: ''
}
Limites de cobertura
${report.limits.coverage
.map((item) => `${htmlEscape(item)} `)
.join('')}
Regras para uso editorial
${report.limits.editorial
.map((item) => `${htmlEscape(item)} `)
.join('')}
${WARNING_ICON}
Nota de defeso eleitoral 2026
Insumo interno não é autorização para publicar.
Publicidade institucional e propaganda eleitoral obedecem a regimes diferentes. Antes de adaptar qualquer trecho para peça externa, submeta texto, imagem, autoria, período e canal à revisão responsável. Este dossiê organiza evidências; não substitui análise jurídica nem editorial.
${sheetFooter(report, 'Fontes e limites · INSUMO INTERNO', pageNo)}
`
const PRINT_CSS = `
* { box-sizing: border-box; }
html, body { margin: 0; padding: 0; }
body { font-family: 'Fira Sans', 'DejaVu Sans', Arial, sans-serif; color: #172033; font-size: 10.5pt; line-height: 1.32; background: #fff; }
@page { size: A4; margin: 0; }
.sheet {
position: relative;
width: 210mm;
min-height: 297mm;
height: 297mm;
padding: 12mm 14mm 10mm;
background: #fff;
display: flex;
flex-direction: column;
overflow: hidden;
break-after: page;
}
.sheet:last-child { break-after: auto; }
.sheet p, .sheet ul, .sheet ol, .sheet dl { margin-top: 0; }
.sheet a { color: #245b78; text-decoration: underline; text-underline-offset: 1.5px; }
.accent-bar { position: absolute; left: 0; top: 0; width: 4mm; height: 297mm; background: #315c75; }
.report-header { display: flex; justify-content: space-between; gap: 8mm; align-items: flex-start; padding-bottom: 3mm; border-bottom: .45mm solid #172033; }
.report-kicker { margin: 0; color: #315c75; font-size: 8pt; font-weight: 700; letter-spacing: .13em; text-transform: uppercase; }
.sheet-title { margin: 1.2mm 0 0; font-size: 18pt; font-weight: 700; line-height: 1.1; letter-spacing: -.02em; }
.sheet-sub { margin: .8mm 0 0; font-size: 10.5pt; color: #435264; }
.report-meta { min-width: 43mm; margin: 0; color: #435264; font-size: 8pt; line-height: 1.45; text-align: right; font-variant-numeric: tabular-nums; }
.report-meta p { margin: 0; }
.report-footer { margin-top: auto; padding-top: 2.5mm; border-top: .25mm solid #cbd5dc; display: flex; justify-content: space-between; gap: 5mm; color: #435264; font-size: 8pt; line-height: 1.3; }
.section-title { margin: 0; font-size: 13.5pt; line-height: 1.18; letter-spacing: -.01em; }
.eyebrow { margin: 0 0 1.4mm; color: #315c75; font-size: 8pt; font-weight: 700; letter-spacing: .1em; text-transform: uppercase; }
.eyebrow.warning-text, .warning-text { color: #8a5a18; }
.muted { color: #435264; }
.small { font-size: 10.5pt; line-height: 1.28; }
.meta { font-size: 7.7pt; line-height: 1.2; color: #435264; }
.strong { font-weight: 600; }
.tabular { font-variant-numeric: tabular-nums; }
.nowrap { white-space: nowrap; }
.sr-only { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); }
.block { margin-top: 3.4mm; break-inside: avoid; }
.block-head { display: flex; align-items: flex-end; justify-content: space-between; gap: 4mm; }
.source-link { display: inline-flex; align-items: baseline; gap: 1mm; white-space: nowrap; font-size: 9pt; font-weight: 600; }
.source-link svg { width: 3mm; height: 3mm; transform: translateY(.35mm); }
.scope { display: inline-flex; align-items: center; min-height: 5mm; padding: .4mm 1.6mm; border: .25mm solid #9aacb7; border-radius: 99px; color: #304758; background: #f7fafb; font-size: 7.5pt; font-weight: 700; letter-spacing: .04em; text-transform: uppercase; }
.scope-region { border-color: #aa8752; color: #6b4918; background: #fff9ee; }
.phase { display: inline-block; padding: .5mm 1.4mm; border-radius: 1mm; background: #e9eef1; color: #334553; font-size: 7.5pt; font-weight: 700; text-transform: uppercase; letter-spacing: .035em; }
.document-table { width: 100%; table-layout: fixed; border-collapse: collapse; font-size: 10.5pt; line-height: 1.22; margin-top: 2.5mm; }
.document-table th { padding: 1.7mm 1.6mm; border-top: .25mm solid #cbd5dc; border-bottom: .35mm solid #9caab3; background: #f3f6f8; color: #435264; font-size: 7.5pt; font-weight: 700; letter-spacing: .045em; text-align: left; text-transform: uppercase; }
.document-table td { padding: 2mm 1.6mm; border-bottom: .25mm solid #dce3e7; vertical-align: top; overflow-wrap: anywhere; }
.document-table tbody tr:nth-child(even) { background: #fafcfd; }
.document-table .numeric { font-variant-numeric: tabular-nums; white-space: nowrap; }
.placeholder-bar { display: inline-block; width: 22mm; height: 2.6mm; border-radius: 1mm; background: #d8e0e5; vertical-align: middle; }
.asset-box { border: .35mm dashed #80919d; color: #4c5c68; background: #f7f9fa; padding: 3mm; display: flex; flex-direction: column; align-items: center; justify-content: center; text-align: center; min-height: 30mm; border-radius: .25rem; font-size: 8pt; font-weight: 600; }
.asset-box p { margin: 0; }
.asset-box .asset-title { text-transform: uppercase; letter-spacing: .08em; }
.timeline-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 1.8mm; margin-top: 2.5mm; }
.timeline-cell { border-top: 1.6px solid #315c75; background: #f3f6f8; padding: 2mm; }
.timeline-period { margin: 0; font-size: 7.6pt; font-weight: 700; color: #315c75; }
.timeline-role { margin: .8mm 0 0; font-size: 10.5pt; font-weight: 600; line-height: 1.16; }
.timeline-recovery { margin: .7mm 0 0; font-size: 7.5pt; line-height: 1.16; color: #435264; }
.deliveries { border-top: .25mm solid #cbd5dc; border-bottom: .25mm solid #cbd5dc; margin-top: 1.8mm; }
.delivery-row { display: grid; grid-template-columns: 26mm 1fr 42mm; gap: 3mm; padding: 2mm 0; border-bottom: .25mm solid #cbd5dc; }
.delivery-row:last-child { border-bottom: none; }
.delivery-title { margin: 0; font-weight: 600; }
.delivery-value { text-align: right; }
.value-big { margin: 0; font-size: 14pt; font-weight: 600; }
.page1-grid { display: grid; grid-template-columns: 1.15fr .85fr; gap: 3mm; margin-top: 3.2mm; break-inside: avoid; }
.panel { border: .25mm solid #cbd5dc; border-radius: .25rem; padding: 3mm; break-inside: avoid; }
.panel-warning { border: none; border-left: 3mm solid #8a5a18; background: #fff7e8; }
.panel-region { border: .35mm solid #c9a66d; background: #fffbf3; }
.tight-list { margin: 0; padding-left: 4mm; }
.tight-list li { margin-bottom: 1mm; }
.aside-accent { border-radius: .25rem; background: #e8f0f4; padding: 3mm; font-size: 10.5pt; line-height: 1.35; }
.era-method { display: grid; grid-template-columns: 1fr 55mm; gap: 4mm; margin-top: 4mm; }
.action-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 3mm; margin-top: 3mm; }
.action-head { display: flex; align-items: center; justify-content: space-between; gap: 3mm; }
.action-title { margin: 2mm 0 0; font-size: 10.5pt; font-weight: 600; }
.honors { display: grid; grid-template-columns: 1fr 58mm; gap: 4mm; }
.rule-warning { display: flex; align-items: flex-start; gap: 3mm; border: .45mm solid #8a5a18; background: #fff7e8; border-radius: .25rem; padding: 4mm; margin-top: 4mm; }
.rule-warning .warn-icon { width: 5mm; height: 5mm; color: #8a5a18; flex-shrink: 0; margin-top: .5mm; }
.rule-title { margin: 0; font-size: 12pt; font-weight: 700; color: #8a5a18; }
.rule-warning .muted { margin-bottom: 0; }
.scope-compare { display: grid; grid-template-columns: 1fr 12mm 1fr; align-items: stretch; gap: 3mm; margin-top: 3mm; }
.compare-mid { display: flex; flex-direction: column; align-items: center; justify-content: center; color: #8a5a18; }
.compare-mid::before, .compare-mid::after { content: ''; width: 1px; flex: 1; background: #d8c09a; }
.compare-mid span { font-size: 18pt; font-weight: 700; margin: 2mm 0; }
.cover-head { display: flex; justify-content: space-between; align-items: flex-start; gap: 8mm; border-bottom: .25mm solid #cbd5dc; padding-bottom: 4mm; }
.cover-series { margin: 2mm 0 0; font-size: 10.5pt; font-weight: 600; letter-spacing: .08em; text-transform: uppercase; color: #435264; }
.internal-note { border: .25mm solid #cbd5dc; background: #f3f6f8; padding: 2mm 3mm; text-align: right; font-size: 8pt; line-height: 1.5; color: #435264; border-radius: .25rem; }
.cover-block { margin-top: 28mm; max-width: 150mm; }
.cover-title { margin: 0; font-size: 34pt; font-weight: 700; line-height: 1.03; letter-spacing: -.035em; }
.cover-sub { margin: 5mm 0 0; max-width: 125mm; font-size: 15pt; line-height: 1.4; color: #435264; }
.cover-dl { display: grid; grid-template-columns: 42mm 1fr; border-top: .25mm solid #cbd5dc; border-bottom: .25mm solid #cbd5dc; margin: 22mm 0 0; font-size: 11pt; }
.cover-dl dt { border-bottom: .25mm solid #cbd5dc; padding: 3mm 0; font-weight: 600; color: #435264; }
.cover-dl dd { margin: 0; border-bottom: .25mm solid #cbd5dc; padding: 3mm 0; font-weight: 600; }
.cover-dl dt:last-of-type, .cover-dl dd:last-of-type { border-bottom: none; }
.cover-grid { display: grid; grid-template-columns: 1fr 56mm; gap: 5mm; margin-top: 6mm; }
.how-to { border-radius: .25rem; background: #e8f0f4; padding: 4mm; }
.how-to .how-to-title { margin: 0; font-size: 10.5pt; font-weight: 700; text-transform: uppercase; letter-spacing: .08em; color: #315c75; }
.how-to p { margin: 2mm 0 0; font-size: 10.5pt; line-height: 1.35; color: #435264; }
.cover-foot { margin-top: auto; }
.cover-foot .ink-rule { height: .5mm; background: #172033; margin-bottom: 5mm; }
.cover-foot-row { display: flex; align-items: flex-end; justify-content: space-between; gap: 8mm; }
.cover-scope { margin: 0; max-width: 118mm; font-size: 10.5pt; line-height: 1.4; color: #435264; }
.cover-version { margin: 0; text-align: right; font-size: 8pt; font-weight: 600; text-transform: uppercase; letter-spacing: .08em; color: #315c75; }
.defeso { margin-top: auto; }
`
export const renderDossierHtml = (report) => {
const gapChunks = chunkGaps(report.gaps)
report.meta.pageTotal = SHEET_COUNT_BASE + report.eras.length + gapChunks.length
let page = 0
const parts = []
parts.push(renderCover(report))
page += 1
parts.push(renderTrajectory(report, ++page))
parts.push(renderSummary(report, ++page))
for (const era of report.eras) parts.push(renderEra(report, era, ++page))
parts.push(renderRegion(report, ++page))
gapChunks.forEach((chunk, index) => parts.push(renderGapsPage(report, chunk, index, ++page)))
parts.push(renderSourcesPage(report, ++page))
return `
${htmlEscape(report.meta.title)} — ${htmlEscape(report.meta.municipalityName)}
${parts.join('\n')}
`
}
const mdEra = (era) => {
const lines = [
`## ${era.label} (${era.period})`,
'',
`_${era.method}_`,
'',
`Trilha: ${era.recovery}`,
]
if (era.numbers.length) {
lines.push(
'',
'| Objeto | Valor | Ano | Fase | Esfera | Fonte |',
'| --- | --- | --- | --- | --- | --- |',
)
for (const row of era.numbers) {
lines.push(
`| ${row.object} | ${row.value ?? '—'} | ${row.year ?? '—'} | ${dossierPhaseLabel(row.phase)} | ${row.sphere} | ${row.sourceUrl ?? '—'} |`,
)
}
}
for (const action of era.actions) {
lines.push(
'',
`- **${stripInlineSources(action.title)}** (${action.sphere}${action.year ? ` · ${action.year}` : ''})${action.detail ? ` — ${stripInlineSources(action.detail)}` : ''} [fonte](${action.sourceUrl})`,
)
}
for (const honor of era.honors)
lines.push('', `- Honraria: ${stripInlineSources(honor.text)} [fonte](${honor.sourceUrl})`)
return lines.join('\n')
}
export const renderDossierMd = (report) => {
const lines = [
`# ${report.meta.title} — ${report.meta.municipalityName}`,
'',
`**Território de identidade:** ${report.meta.region}`,
`**Gerado em:** ${formatDateTimeBr(report.meta.generatedAt)} · **Base Teqo (read-only) lida em:** ${report.meta.readAtLabel}`,
'',
'> INSUMO INTERNO — não circular, não publicar. Documento de trabalho.',
'',
'## Linha do tempo da carreira',
'',
]
for (const row of report.trajectory) {
lines.push(
`- **${row.period}** — ${row.role}${row.uncertain ? ` _(${row.uncertain})_` : ''} · ${row.recovery}`,
)
}
if (report.page1.deliveries.length) {
lines.push('', '## Principais entregas localizadas', '')
for (const row of report.page1.deliveries) {
lines.push(
`- **${stripInlineSources(row.title)}** · _${row.sphere}_${row.value ? ` · ${row.value}` : ''}${row.phase ? ` (${dossierPhaseLabel(row.phase)})` : ''}${row.detail ? ` — ${stripInlineSources(row.detail)}` : ''} [fonte](${row.sourceUrl})`,
)
}
lines.push('', '_Autorizado ≠ empenhado ≠ liquidado ≠ pago. A fase acompanha cada valor._')
}
for (const era of report.eras) {
lines.push('', mdEra(era))
}
lines.push(
'',
'## Região / polo',
'',
`> ${report.region.ruleTitle}`,
`> ${report.region.ruleBody}`,
'',
)
lines.push('### Município', '')
for (const item of report.region.municipal.items) {
lines.push(`- ${stripInlineSources(item.answer)} — [fonte](${item.sourceUrl})`)
}
lines.push('', '### Região / polo (não somável)', '')
for (const item of report.region.regional.items) {
lines.push(`- ${stripInlineSources(item.answer)} — [fonte](${item.sourceUrl})`)
}
if (report.region.items.length) {
lines.push('', '| Item | Esfera | Evidência | Fonte |', '| --- | --- | --- | --- |')
for (const item of report.region.items) {
lines.push(
`| ${stripInlineSources(item.item)} | ${item.sphere} | ${stripInlineSources(item.evidence)} | ${item.sourceUrl} |`,
)
}
}
lines.push('', '## Lacunas explícitas', '')
if (report.gaps.length === 0) lines.push('_Nenhuma lacuna declarada._')
for (const gap of report.gaps) {
lines.push(`- **${gap.label}:** ${gap.reason} → ${gap.nextStep}`)
}
if (report.news.length) {
lines.push('', '## Notícias e documentos consultados', '')
for (const row of report.news) {
lines.push(`- ${formatDateBr(row.date)} · ${row.outlet} — ${row.title} — ${row.url}`)
}
}
lines.push(
'',
'## Limites de cobertura',
'',
...report.limits.coverage.map((item) => `- ${item}`),
'',
'## Regras para uso editorial',
'',
...report.limits.editorial.map((item) => `- ${item}`),
'',
'## Nota de defeso eleitoral 2026',
'',
'Insumo interno não é autorização para publicar. Publicidade institucional e propaganda eleitoral obedecem a regimes diferentes. Antes de adaptar qualquer trecho para peça externa, submeta texto, imagem, autoria, período e canal à revisão responsável.',
'',
`_Documento interno de campanha, datado (${formatDateTimeBr(report.meta.generatedAt)}). O dado envelhece: confira a data de leitura da base._`,
)
return `${lines.join('\n')}\n`
}