/**
* 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, dossierSphereBadgeClass, dossierSphereLabel } from './dossieBlocks.mjs'
import { MUNICIPALITY_UNIT, isInstitutionUnit } from './dossieUnit.mjs'
import { htmlEscape, moreItemsLabel, showingLabel, 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, unit = MUNICIPALITY_UNIT) => {
const badgeClass = dossierSphereBadgeClass(sphere, unit)
return `${htmlEscape(label ?? dossierSphereLabel(sphere, unit))} `
}
/** `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
}
/**
* Print surfaces prefer the reformulated `brief` (short headline + note that fit
* the fixed A4 with no ellipsis) and fall back to the full `answer`/`details`
* record when an item has none — the builder's A4 fit guard then fails closed
* instead of silently dropping information.
*/
const copyHtml = (value) => htmlEscape(stripInlineSources(value))
const briefOr = (brief, fallback) => brief?.title ?? fallback
const noteOr = (brief, fallback) => brief?.note ?? fallback
const PHASE_MODIFIER = {
autorizado: 'phase-pending',
empenhado: 'phase-pending',
liquidado: 'phase-paid',
pago: 'phase-paid',
}
const phaseBadge = (phase) => {
const modifier = PHASE_MODIFIER[phase]
return `${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: 7 full-text rows per A4 sheet. */
const GAPS_PER_PAGE = 7
/** News are paginated, never truncated: 10 full-title rows per A4 sheet. */
const NEWS_PER_PAGE = 10
/** Era "Atuação registrada" cards: 2 on the first sheet, 6 per continuation sheet. */
const ERA_ACTIONS_FIRST_SHEET = 2
const ERA_ACTIONS_PER_SHEET = 6
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 chunkNews = (news) => {
const list = Array.isArray(news) ? news : []
const chunks = []
for (let index = 0; index < list.length; index += NEWS_PER_PAGE) {
chunks.push(list.slice(index, index + NEWS_PER_PAGE))
}
return chunks
}
/**
* Era action cards, paginated so no sourced fact is dropped: the first sheet
* carries the design's 2 cards (with method + numbers + honors), and the rest
* flow to continuation sheets.
*/
const chunkEraActions = (era) => {
const actions = Array.isArray(era.actions) ? era.actions : []
const first = actions.slice(0, ERA_ACTIONS_FIRST_SHEET)
const rest = actions.slice(ERA_ACTIONS_FIRST_SHEET)
const chunks = [first]
for (let index = 0; index < rest.length; index += ERA_ACTIONS_PER_SHEET) {
chunks.push(rest.slice(index, index + ERA_ACTIONS_PER_SHEET))
}
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) => {
const deliveries = report.page1.deliveries
if (deliveries.items.length === 0) return ''
return `
02 · destaques com lastro
Principais entregas localizadas
${deliveries.items
.map(
(row) => `
${scopeBadge(row.sphere)}
Era ${htmlEscape(row.era)}${row.year ? ` · ${htmlEscape(row.year)}` : ''}
${copyHtml(briefOr(row.brief, row.title))}
${noteOr(row.brief, row.detail) ? `
${copyHtml(noteOr(row.brief, row.detail))}
` : ''}
${valueCell(row.value, 'Valor não informado')}
${row.phase ? phaseBadge(row.phase) : ''}
${sourceLink(row.sourceUrl)}
`,
)
.join('')}
${
deliveries.remaining > 0
? `${htmlEscape(showingLabel(deliveries.items.length, deliveries.total, 'entrega localizada', 'entregas localizadas'))} — o restante está nas páginas das eras.
`
: ''
}
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.items.length === 0 && pending.items.length === 0) return ''
return `
${
hooks.items.length
? `
03 · ganchos para o boletim
${hooks.items
.map(
(hook) =>
`Tema: ${htmlEscape(hook.topic)}. Ângulo: ${copyHtml(briefOr(hook.brief, hook.angle))} ${sourceLink(hook.sourceUrl)} `,
)
.join('')}
${hooks.remaining > 0 ? `
${htmlEscape(moreItemsLabel(hooks.remaining, 'gancho', 'ganchos'))} nas eras.
` : ''}
`
: '
'
}
${
pending.items.length
? `
04 · o que falta
${pending.items.map((item) => `${htmlEscape(item)} `).join('')}
${pending.remaining > 0 ? `
${htmlEscape(moreItemsLabel(pending.remaining, 'ponto', 'pontos'))} sem leitura na tabela de lacunas.
` : ''}
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 renderActionGrid = (actions) => `
${actions
.map(
(action) => `
${scopeBadge(action.sphere)}${htmlEscape(action.year ?? '—')}
${copyHtml(briefOr(action.brief, action.title))}
${noteOr(action.brief, action.detail) ? `${copyHtml(noteOr(action.brief, action.detail))}
` : ''}
${sourceLink(action.sourceUrl)}
`,
)
.join('')}
`
const renderEraPage = (report, era, actions, index, pageNo) => {
const first = index === 0
const slug = era.id.toLowerCase()
const anchor = first ? `era-${slug}` : `era-${slug}-${index + 1}`
const subtitle = first
? `${era.period} · ${era.subtitle}`
: `Atuação registrada · continuação ${index + 1}`
return `
${sheetHeader(report, era.label, subtitle, pageNo)}
${
first
? `
Recorte e método
${htmlEscape(era.method)}
Trilha de recuperação ${htmlEscape(era.recovery)}
${
era.numbers.items.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.items
.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('')}
${era.numbers.remaining > 0 ? `${htmlEscape(moreItemsLabel(era.numbers.remaining, 'número com fase', 'números com fase'))} — a lista completa está no arquivo de pesquisa da era.
` : ''}
Não consolidar fases como se fossem equivalentes. Um valor empenhado não é um valor pago.
`
: ''
}`
: ''
}
${
actions.length
? `
Atuação registrada
O que fez — item, alcance e lastro
${renderActionGrid(actions)}
${era.camaraActionsRemaining > 0 ? `${htmlEscape(moreItemsLabel(era.camaraActionsRemaining, 'ação da Câmara', 'ações da Câmara'))} — a coleta é limitada às páginas consultadas; o restante está no acervo da Câmara.
` : ''}
`
: ''
}
${
first && era.honors.length
? `
Títulos, honrarias e vínculos locais
${era.honors
.map(
(honor) =>
`${copyHtml(briefOr(honor.brief, 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) =>
`${copyHtml(item.brief?.title ?? item.answer)} ${sourceLink(item.sourceUrl)} `,
)
.join(
'',
)}${report.region.municipal.items.length === 0 ? 'Nenhum item municipal com fonte. ' : ''}
${report.region.municipal.remaining > 0 ? `
${htmlEscape(moreItemsLabel(report.region.municipal.remaining, 'item municipal com fonte', 'itens municipais com fonte'))} — ver a tabela de evidências abaixo.
` : ''}
≠
${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) =>
`${copyHtml(item.brief?.title ?? item.answer)} ${sourceLink(item.sourceUrl)} `,
)
.join(
'',
)}${report.region.regional.items.length === 0 ? 'Nenhum item regional com fonte. ' : ''}
${report.region.regional.remaining > 0 ? `
${htmlEscape(moreItemsLabel(report.region.regional.remaining, 'item regional com fonte', 'itens regionais com fonte'))} — ver a tabela de evidências abaixo.
` : ''}
`
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) => `${copyHtml(item.detail)} ${sourceLink(item.sourceUrl, `(${item.topic})`)}`,
)
.join(' · ')} — leitura relativa, nunca % estadual absoluto.
`
: ''
}
${renderScopeLists(report)}
${
report.region.evidence.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.evidence.items
.map(
(item) => `
${copyHtml(briefOr(item.brief, item.item))}
${scopeBadge(item.sphere)}
${copyHtml(noteOr(item.brief, item.evidence))}
${sourceLink(item.sourceUrl)}
`,
)
.join('')}
${report.region.evidence.remaining > 0 ? `${htmlEscape(showingLabel(report.region.evidence.items.length, report.region.evidence.total, 'item regional recuperado', 'itens regionais recuperados'))} em evidência; o restante está no arquivo de pesquisa da era.
` : ''}
`
: ''
}
${
report.region.hook || report.region.priorityGap
? `
${
report.region.hook
? `Gancho possível
Tema: ${htmlEscape(report.region.hook.topic)}. Ângulo: ${copyHtml(briefOr(report.region.hook.brief, 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 renderNewsPage = (report, chunk, index, pageNo) => `
${sheetHeader(report, 'Notícias e documentos consultados', `Fontes externas datadas${index > 0 ? ` (continuação ${index + 1})` : ''}`, pageNo)}
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
${chunk
.map(
(row) => `
${htmlEscape(formatShortDateBr(row.date))}
${htmlEscape(row.outlet)}
${htmlEscape(row.title)}
${sourceLink(row.url, '(fonte)')}
`,
)
.join('')}
${sheetFooter(report, 'Notícias e documentos consultados · 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)}
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; }
.cover-accent { color: #315c75; }
.defeso { margin-top: auto; }
`
const renderMunicipalityDossierHtml = (report) => {
const gapChunks = chunkGaps(report.gaps)
const newsChunks = chunkNews(report.news)
const eraSheets = report.eras.map((era) => ({ era, chunks: chunkEraActions(era) }))
const eraSheetCount = eraSheets.reduce((sum, entry) => sum + entry.chunks.length, 0)
report.meta.pageTotal = SHEET_COUNT_BASE + eraSheetCount + gapChunks.length + newsChunks.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, chunks } of eraSheets) {
chunks.forEach((actions, index) =>
parts.push(renderEraPage(report, era, actions, index, ++page)),
)
}
parts.push(renderRegion(report, ++page))
gapChunks.forEach((chunk, index) => parts.push(renderGapsPage(report, chunk, index, ++page)))
newsChunks.forEach((chunk, index) => parts.push(renderNewsPage(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.items.length) {
lines.push(
'',
'| Objeto | Valor | Ano | Fase | Esfera | Fonte |',
'| --- | --- | --- | --- | --- | --- |',
)
for (const row of era.numbers.items) {
lines.push(
`| ${row.object} | ${row.value ?? '—'} | ${row.year ?? '—'} | ${dossierPhaseLabel(row.phase)} | ${row.sphere} | ${row.sourceUrl ?? '—'} |`,
)
}
if (era.numbers.remaining > 0) {
lines.push(
'',
`_${moreItemsLabel(era.numbers.remaining, 'número com fase', 'números com fase')} não exibidos — ver as fontes._`,
)
}
}
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})`,
)
}
if (era.camaraActionsRemaining > 0) {
lines.push(
'',
`_${moreItemsLabel(era.camaraActionsRemaining, 'ação da Câmara', 'ações da Câmara')} — a coleta é limitada às páginas consultadas._`,
)
}
for (const honor of era.honors)
lines.push('', `- Honraria: ${stripInlineSources(honor.text)} [fonte](${honor.sourceUrl})`)
return lines.join('\n')
}
const renderMunicipalityDossierMd = (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.items.length) {
lines.push('', '## Principais entregas localizadas', '')
for (const row of report.page1.deliveries.items) {
lines.push(
`- **${stripInlineSources(row.title)}** · _${row.sphere}_${row.value ? ` · ${row.value}` : ''}${row.phase ? ` (${dossierPhaseLabel(row.phase)})` : ''}${row.detail ? ` — ${stripInlineSources(row.detail)}` : ''} [fonte](${row.sourceUrl})`,
)
}
if (report.page1.deliveries.remaining > 0) {
lines.push(
'',
`_${showingLabel(report.page1.deliveries.items.length, report.page1.deliveries.total, 'entrega localizada', 'entregas localizadas')} — o restante está nas páginas das eras._`,
)
}
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})`)
}
if (report.region.municipal.remaining > 0) {
lines.push(
`- _${moreItemsLabel(report.region.municipal.remaining, 'item municipal com fonte', 'itens municipais com fonte')} — ver a tabela de evidências._`,
)
}
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.regional.remaining > 0) {
lines.push(
`- _${moreItemsLabel(report.region.regional.remaining, 'item regional com fonte', 'itens regionais com fonte')} — ver a tabela de evidências._`,
)
}
if (report.region.evidence.items.length) {
lines.push('', '| Item | Esfera | Evidência | Fonte |', '| --- | --- | --- | --- |')
for (const item of report.region.evidence.items) {
lines.push(
`| ${stripInlineSources(item.item)} | ${item.sphere} | ${stripInlineSources(item.evidence)} | ${item.sourceUrl} |`,
)
}
if (report.region.evidence.remaining > 0) {
lines.push(
'',
`_${showingLabel(report.region.evidence.items.length, report.region.evidence.total, 'item regional recuperado', 'itens regionais recuperados')} em evidência; o restante está no arquivo de pesquisa da era._`,
)
}
}
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`
}
/* ------------------------------------------------------------------ *
* Institution renderer (C187) — ports the approved institutional *
* hi-fi (`dossie-solla-instituicao-ui-design.html`) class-by-class; *
* shares the print shell, source tokens and table CSS of the city *
* renderer (one owner). *
* ------------------------------------------------------------------ */
const INSTITUTION_PRINT_CSS = `
${PRINT_CSS}
.identity-badge { display: inline-flex; align-items: center; min-height: 5mm; padding: .45mm 1.6mm; border: .25mm solid #cbd5dc; border-radius: 99px; color: #435264; background: #f3f6f8; font-size: 7.4pt; line-height: 1; font-weight: 700; letter-spacing: .035em; text-transform: uppercase; }
.scope-sector { border-color: #9d8b64; color: #65501f; background: #fffaf0; }
.scope-network { border-color: #8f819c; color: #554362; background: #faf7fc; }
.phase-paid { color: #285338; background: #e6f2e9; }
.phase-pending { color: #6b4918; background: #fff2d9; }
.empty-panel { border: .45mm dashed #b18549; color: #684819; background: #fff7e8; border-radius: .25rem; padding: 5mm 8mm; text-align: center; }
.empty-panel .rule-title { margin-top: 1.5mm; }
.empty-panel .warn-icon { width: 8mm; height: 8mm; margin: 0 auto; color: #8a5a18; }
.timeline-item { position: relative; padding-left: 5mm; }
.timeline-item::before { content: ''; position: absolute; top: 1.5mm; left: 0; width: 2.6mm; height: 2.6mm; border: .5mm solid #fff; border-radius: 99px; background: #315c75; box-shadow: 0 0 0 .35mm #315c75; }
.id-card { border: .25mm solid #cbd5dc; border-radius: .25rem; background: #f3f6f8; padding: 3mm; }
.id-card dl { display: grid; grid-template-columns: 22mm 1fr; gap: 1mm 2mm; font-size: 9pt; line-height: 1.3; }
.id-card dt { font-weight: 600; color: #435264; }
.id-card dd { margin: 0; }
.summary-top { display: grid; grid-template-columns: 58mm 1fr; gap: 4mm; margin-top: 4mm; }
.vinc-timeline { display: grid; grid-template-columns: repeat(3, 1fr); gap: 2mm; border-top: .25mm solid #cbd5dc; padding-top: 3mm; margin-top: 3mm; }
.vinc-period { margin: 0; font-size: 8pt; font-weight: 700; color: #315c75; }
.vinc-text { margin: 1mm 0 0; font-size: 9pt; font-weight: 600; line-height: 1.25; }
.vinc-source { margin: 1mm 0 0; font-size: 8pt; line-height: 1.2; color: #435264; }
.scope-cards { display: grid; grid-template-columns: repeat(3, 1fr); gap: 3mm; margin-top: 4mm; }
.scope-card { border: .25mm solid #cbd5dc; border-radius: .25rem; padding: 3mm; }
.scope-card.card-sector { border-color: #c9b483; background: #fffcf6; }
.scope-card.card-network { border-color: #b9acc3; background: #fcf9fd; }
.scope-count { margin: 3mm 0 0; font-size: 16pt; font-weight: 600; font-variant-numeric: tabular-nums; }
.scope-cards .tight-list { margin-top: 2mm; }
.more-note { margin: 1.5mm 0 0; font-size: 8pt; font-weight: 600; color: #8a5a18; }
`
const instHeader = (report, title, subtitle, pageNo) => `
`
const instFooter = (report, left, pageNo) => `
`
/** Singular of the capped-list nouns (never derive it by chopping the "s"). */
const CAPPED_NOUNS = {
itens: 'item',
entregas: 'entrega',
vínculos: 'vínculo',
}
const moreNote = (cappedList, noun = 'itens') => {
if (cappedList.omitted <= 0) return ''
const label = cappedList.omitted === 1 ? (CAPPED_NOUNS[noun] ?? noun) : noun
return `e mais ${cappedList.omitted} ${label} com fonte fora desta página — nada foi descartado.
`
}
const identityBadgeList = (report) =>
report.meta.identityBadges
.map((badge) => `${htmlEscape(badge)} `)
.join(' ')
const renderInstitutionCover = (report) => `
${htmlEscape(report.cover.kicker)}
${htmlEscape(report.cover.series)}
${htmlEscape(report.cover.internalNote)} ${htmlEscape(report.cover.internalNoteSub)}
${htmlEscape(report.cover.eyebrow)}
Dossiê Solla —${htmlEscape(report.cover.title)}
${htmlEscape(report.cover.subtitle)}
Identificação ${identityBadgeList(report)}
Data de geração ${htmlEscape(formatDateTimeBr(report.meta.generatedAt))}
Escopo e versão Eras A / B / C · instituição, setor e rede separados · versão 01
Como usar
${htmlEscape(report.cover.howToUse)}
${assetBox('NEEDS ASSET', ['selo da instituição', 'uso autorizado e origem'])}
`
const renderInstitutionIdentity = (report) => {
const identity = report.meta.identity ?? {}
const rows = [
['Nome', identity.name],
['Tipo', identity.kindLabel],
['Esfera', identity.sphereLabel],
['Alcance', identity.scopeLabel],
['Alias', (identity.aliases ?? []).join(' · ') || '—'],
].filter(([, value]) => Boolean(value))
return `
Identificação
${rows.map(([key, value]) => `${htmlEscape(key)} ${htmlEscape(value)} `).join('')}
`
}
const renderInstitutionVincTimeline = (report) => {
const timeline = report.page1.timeline
if (timeline.items.length === 0) {
return 'Nenhum vínculo datado com fonte — ver lacunas explícitas.
'
}
return `
${timeline.items
.map(
(row) => `
${htmlEscape(row.period || '—')}
${copyHtml(briefOr(row.brief, row.role))}
${htmlEscape(row.source ?? '')} ${sourceLink(row.url)}
`,
)
.join('')}
${moreNote(timeline, 'vínculos')}`
}
const renderInstitutionDeliveries = (report) => {
const deliveries = report.page1.deliveries
if (deliveries.items.length === 0) return ''
return `
02 · destaques com lastro
Principais entregas localizadas
Cada valor mantém a fase e a abrangência
${deliveries.items
.map(
(row) => `
${scopeBadge(row.sphere, null, report.unit)}
Era ${htmlEscape(row.era)}${row.year ? ` · ${htmlEscape(row.year)}` : ''}
${copyHtml(briefOr(row.brief, row.title))}
${noteOr(row.brief, row.detail) ? `
${copyHtml(noteOr(row.brief, row.detail))}
` : ''}
${valueCell(row.value, 'Valor não informado')}
${row.phase ? phaseBadge(row.phase) : ''}
${sourceLink(row.sourceUrl)}
`,
)
.join('')}
${moreNote(deliveries, 'entregas')}
Guarda de leitura: autorizado ≠ empenhado ≠ liquidado ≠ pago. A fase acompanha cada valor.
`
}
const renderInstitutionHooksAndPending = (report) => {
const hooks = report.page1.hooks
const pending = report.page1.pending
if (hooks.items.length === 0 && pending.items.length === 0) return ''
return `
${
hooks.items.length
? `
Gancho para a agenda
${hooks.items
.map(
(hook) =>
`${htmlEscape(hook.topic)}: ${copyHtml(briefOr(hook.brief, hook.angle))} ${sourceLink(hook.sourceUrl)} `,
)
.join('')}
`
: '
'
}
${
pending.items.length
? `
O que falta
${pending.items.map((item) => `${htmlEscape(item)} `).join('')}
Ausência é resultado: não completar por inferência.
`
: '
'
}
`
}
const renderInstitutionSummary = (report, pageNo) => `
${instHeader(report, 'Resumo de uma olhada', 'Informação datada e pronta para conferência', pageNo)}
${renderInstitutionIdentity(report)}
01 · vínculo com a instituição
Linha do tempo documentada
${renderInstitutionVincTimeline(report)}
${renderInstitutionDeliveries(report)}
${renderInstitutionHooksAndPending(report)}
${instFooter(report, 'Resumo · INSUMO INTERNO', pageNo)}
`
const renderInstitutionEraEmpty = (report, era, pageNo) => `
${instHeader(report, `${era.label} · Pesquisa sem evidência suficiente`, era.period, pageNo)}
Recorte previsto
${htmlEscape(era.method)}
Trilha tentada ${htmlEscape(era.recovery)}
${WARNING_ICON}
Lacuna explícita · não é “zero”
Nenhuma fonte suficiente foi localizada para esta era.
A pesquisa não encontrou documento que sustente uma afirmação institucional. Isso não prova que não houve vínculo ou atuação; prova apenas que o dossiê ainda não pode afirmá-los.
O que foi feito
Busca nominal por instituição e aliases.
Consulta a fontes oficiais e acervos datados.
Triagem de resultados por vínculo e abrangência.
Descarte de menções sem lastro suficiente.
O que falta fazer
Solicitar consulta ao arquivo físico.
Validar nome histórico e unidade da instituição.
Recuperar ato, ata ou documento contemporâneo.
Repetir busca com alias confirmado.
${instFooter(report, `${era.label} · lacuna explícita`, pageNo)}
`
const renderInstitutionEra = (report, era, pageNo) => {
if (era.empty) return renderInstitutionEraEmpty(report, era, pageNo)
return `
${instHeader(report, era.label, `${era.period} · ${era.subtitle}`, pageNo)}
Recorte e método
${htmlEscape(era.method)}
Trilha de recuperação ${htmlEscape(era.recovery)}
${
era.numbers.items.length
? `
Entregas localizadas
Objeto, valor, fase e alcance
Valores por ano, fase, abrangência e fonte
Objeto Valor Ano Fase Abrangência Fonte
${era.numbers.items
.map(
(row) => `
${htmlEscape(row.object)}
${valueCell(row.value, 'Valor a recuperar')}
${htmlEscape(row.year ?? '—')}
${phaseBadge(row.phase)}
${scopeBadge(row.sphere, null, report.unit)}
${sourceLink(row.sourceUrl)}${row.sphere !== 'instituicao' ? 'não somar à instituição ' : ''}
`,
)
.join('')}
Não consolidar fases como se fossem equivalentes. Um valor empenhado não é um valor pago.
`
: ''
}
${
era.actions.items.length
? `
Atuação / vínculo registrado
Papéis com evidência visível
${era.actions.items
.map(
(action) => `
${scopeBadge(action.sphere, null, report.unit)}${htmlEscape(action.year ?? '—')}
${copyHtml(briefOr(action.brief, action.title))}
${noteOr(action.brief, action.detail) ? `${copyHtml(noteOr(action.brief, action.detail))}
` : ''}
${sourceLink(action.sourceUrl)}
`,
)
.join('')}
`
: ''
}
${
era.honors.length
? `
Títulos, honrarias e vínculos institucionais
${era.honors
.map(
(honor) =>
`${copyHtml(briefOr(honor.brief, honor.text))} ${sourceLink(honor.sourceUrl)} `,
)
.join('')}
${assetBox('NEEDS ASSET', ['reprodução do ato ou clipping', 'somente com origem e licença'])}
`
: ''
}
${instFooter(report, era.label, pageNo)}
`
}
const renderInstitutionScope = (report, pageNo) => {
const reach = report.reach
const cardClass = { institution: '', sector: 'card-sector', network: 'card-network' }
return `
${instHeader(report, 'Abrangência: instituição × setor × rede', 'Três recortes, três leituras, nenhuma soma combinada', pageNo)}
${WARNING_ICON}
${htmlEscape(reach.ruleTitle)}
${htmlEscape(reach.ruleBody)}
Leitura lado a lado
O alcance acompanha cada evidência
${reach.lists
.map(
(list) => `
${scopeBadge(list.sphere, null, report.unit)}${list.sphere === 'instituicao' ? 'DIRETO' : 'NÃO SOMAR'}
${list.total} ${list.total === 1 ? 'item' : 'itens'}
com fonte
${list.items
.map(
(item) =>
`${copyHtml(item.brief?.title ?? item.answer)} ${sourceLink(item.sourceUrl)} `,
)
.join(
'',
)}${list.items.length === 0 ? 'Nenhum item com fonte. ' : ''}
${moreNote(list, 'itens')}
`,
)
.join('')}
${
reach.evidence.items.length
? `
Evidência de alcance
Por que cada item está neste recorte Sem evidência, vira lacuna
Itens de setor e rede com evidência de alcance
Item Abrangência Evidência de alcance Fonte
${reach.evidence.items
.map(
(row) => `
${copyHtml(briefOr(row.brief, row.item))}
${scopeBadge(row.sphere, null, report.unit)}
${copyHtml(row.evidence)}
${sourceLink(row.sourceUrl)}
`,
)
.join('')}
${moreNote(reach.evidence, 'itens')}
`
: ''
}
${
reach.hook || reach.priorityGap
? `
${
reach.hook
? `Gancho possível
${htmlEscape(reach.hook.topic)}: ${copyHtml(briefOr(reach.hook.brief, reach.hook.angle))} ${sourceLink(reach.hook.sourceUrl)}
`
: '
'
}
${
reach.priorityGap
? `Lacuna prioritária
${htmlEscape(reach.priorityGap)}
`
: '
'
}
`
: ''
}
${instFooter(report, 'Abrangência · recortes não somáveis', pageNo)}
`
}
const honorDate = (honor) => {
if (honor.date) return formatDateBr(honor.date)
if (honor.year) return String(honor.year)
return '—'
}
const institutionHonors = (report) => report.eras.find((era) => era.id === 'C')?.honors ?? []
const hasInstitutionHonors = (report) => institutionHonors(report).length > 0
const renderInstitutionHonors = (report, pageNo) => {
const honors = institutionHonors(report)
if (honors.length === 0) return ''
const gap = report.gaps?.[0] ?? null
const hook = report.reach.hook ?? report.page1.hooks.items[0] ?? null
return `
${instHeader(report, 'Títulos, honrarias e vínculos', 'Reconhecimento não substitui entrega; vínculo não prova resultado', pageNo)}
Registros confirmados
Data, natureza e fonte primária
Títulos, honrarias e vínculos por data e fonte
Data Natureza Registro Fonte
${honors
.map(
(honor) => `
${htmlEscape(honorDate(honor))}
reconhecimento
${copyHtml(briefOr(honor.brief, honor.text))} ${noteOr(honor.brief, honor.detail) ? `${copyHtml(noteOr(honor.brief, honor.detail))} ` : ''}
${sourceLink(honor.sourceUrl)}
`,
)
.join('')}
Como ler
Vínculo documentado
Informa papel, unidade e período. Não autoriza atribuir toda ação posterior da instituição.
Honraria documentada
Registra reconhecimento e órgão concedente. Não vira prova de recurso ou entrega.
${assetBox('NEEDS ASSET', ['reprodução do ato, diploma ou clipping', 'origem, autoria e licença'])}
Gancho para a agenda
${hook ? `${htmlEscape(hook.topic)}: ${copyHtml(briefOr(hook.brief, hook.angle))}` : 'Mencionar um vínculo só na formulação que a fonte sustenta e conectá-lo a uma entrega comprovada, se houver.'}
Pendência documental
${gap ? `${htmlEscape(gap.label)}: ${htmlEscape(gap.reason)}` : 'Nenhuma pendência declarada para os registros desta página.'}
${instFooter(report, 'Títulos e vínculos · INSUMO INTERNO', pageNo)}
`
}
const renderInstitutionGapsPage = (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 `
${instHeader(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}
${instFooter(report, 'Lacunas explícitas · INSUMO INTERNO', pageNo)}
`
}
const renderInstitutionSourcesPage = (report, pageNo) => `
${instHeader(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: 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('')}
`
: ''
}
${
report.acervo?.items?.length
? `
Acervo interno (read-only)
Falas do mandato sobre o recorte Base Teqo, 2011+
${report.acervo.items
.map(
(row) =>
`${htmlEscape(row.period)} — ${copyHtml(row.text)}${row.excerpt ? ` (${copyHtml(row.excerpt)}) ` : ''} ${sourceLink(row.sourceUrl)} `,
)
.join('')}
${moreNote(report.acervo, 'itens')}
`
: ''
}
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.
${instFooter(report, 'Fontes e limites · INSUMO INTERNO', pageNo)}
`
const renderInstitutionDossierHtml = (report) => {
const gapChunks = chunkGaps(report.gaps)
report.meta.pageTotal =
1 + // capa
1 + // resumo
report.eras.length +
1 + // abrangência
(hasInstitutionHonors(report) ? 1 : 0) +
gapChunks.length +
1 // fontes
let page = 0
const parts = []
parts.push(renderInstitutionCover(report))
page += 1
parts.push(renderInstitutionSummary(report, ++page))
for (const era of report.eras) parts.push(renderInstitutionEra(report, era, ++page))
parts.push(renderInstitutionScope(report, ++page))
if (hasInstitutionHonors(report)) parts.push(renderInstitutionHonors(report, ++page))
gapChunks.forEach((chunk, index) =>
parts.push(renderInstitutionGapsPage(report, chunk, index, ++page)),
)
parts.push(renderInstitutionSourcesPage(report, ++page))
return `
${htmlEscape(report.meta.title)} — ${htmlEscape(report.meta.subjectName)}
${parts.join('\n')}
`
}
const institutionMdEra = (era) => {
const lines = [
`## ${era.label} (${era.period})`,
'',
`_${era.method}_`,
'',
`Trilha: ${era.recovery}`,
]
if (era.empty) {
lines.push(
'',
`> Lacuna explícita: nenhuma fonte suficiente foi localizada para a ${era.label}. Isso não prova ausência — prova que o dossiê ainda não pode afirmar.`,
)
return lines.join('\n')
}
if (era.numbers.items.length) {
lines.push(
'',
'| Objeto | Valor | Ano | Fase | Abrangência | Fonte |',
'| --- | --- | --- | --- | --- | --- |',
)
for (const row of era.numbers.items) {
lines.push(
`| ${row.object} | ${row.value ?? '—'} | ${row.year ?? '—'} | ${dossierPhaseLabel(row.phase)} | ${row.sphere} | ${row.sourceUrl ?? '—'} |`,
)
}
}
for (const action of era.actions.items) {
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')
}
const renderInstitutionDossierMd = (report) => {
const lines = [
`# ${report.meta.title} — ${report.meta.subjectName}`,
'',
`**Identificação:** ${report.meta.identityBadges.join(' · ')}`,
`**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.',
'',
`## Identificação`,
'',
`- **Nome:** ${report.meta.identity?.name ?? '—'}`,
`- **Tipo:** ${report.meta.identity?.kindLabel ?? '—'}`,
`- **Esfera:** ${report.meta.identity?.sphereLabel ?? '—'}`,
`- **Alcance:** ${report.meta.identity?.scopeLabel ?? '—'}`,
`- **Alias:** ${(report.meta.identity?.aliases ?? []).join(' · ') || '—'}`,
'',
'## Linha do tempo documentada',
'',
]
if (report.page1.timeline.items.length === 0) lines.push('_Nenhum vínculo datado com fonte._')
for (const row of report.page1.timeline.items) {
lines.push(
`- **${row.period || '—'}** — ${stripInlineSources(row.role)} (${row.source ?? ''}) [fonte](${row.url})`,
)
}
if (report.page1.timeline.omitted > 0)
lines.push(`- _e mais ${report.page1.timeline.omitted} vínculo(s) com fonte._`)
if (report.page1.deliveries.items.length) {
lines.push('', '## Principais entregas localizadas', '')
for (const row of report.page1.deliveries.items) {
lines.push(
`- **${stripInlineSources(row.title)}** · _${row.sphere}_${row.value ? ` · ${row.value}` : ''}${row.phase ? ` (${dossierPhaseLabel(row.phase)})` : ''}${row.detail ? ` — ${stripInlineSources(row.detail)}` : ''} [fonte](${row.sourceUrl})`,
)
}
if (report.page1.deliveries.omitted > 0)
lines.push(`- _e mais ${report.page1.deliveries.omitted} entrega(s) com fonte._`)
lines.push('', '_Autorizado ≠ empenhado ≠ liquidado ≠ pago. A fase acompanha cada valor._')
}
for (const era of report.eras) lines.push('', institutionMdEra(era))
lines.push('', '## Abrangência: instituição × setor × rede', '')
lines.push(`> ${report.reach.ruleTitle}`, `> ${report.reach.ruleBody}`, '')
for (const list of report.reach.lists) {
lines.push(`### ${list.label}`, '')
if (list.items.length === 0) lines.push('- Nenhum item com fonte.')
for (const item of list.items) {
lines.push(`- ${stripInlineSources(item.answer)} — [fonte](${item.sourceUrl})`)
}
if (list.omitted > 0) lines.push(`- _e mais ${list.omitted} item(ns) com fonte._`)
lines.push('')
}
if (report.reach.evidence.items.length) {
lines.push('| Item | Abrangência | Evidência | Fonte |', '| --- | --- | --- | --- |')
for (const item of report.reach.evidence.items) {
lines.push(
`| ${stripInlineSources(item.item)} | ${item.sphere} | ${stripInlineSources(item.evidence)} | ${item.sourceUrl} |`,
)
}
}
if (report.acervo?.items?.length) {
lines.push('', '## Acervo interno (read-only)', '')
for (const row of report.acervo.items) {
lines.push(
`- **${row.period}** — ${stripInlineSources(row.text)}${row.excerpt ? ` _(${stripInlineSources(row.excerpt)})_` : ''} [fonte](${row.sourceUrl})`,
)
}
if (report.acervo.omitted > 0)
lines.push(`- _e mais ${report.acervo.omitted} fala(s) com fonte._`)
}
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`
}
/** Dispatch: institution reports render with the institutional sections. */
export const renderDossierHtml = (report) =>
isInstitutionUnit(report.unit)
? renderInstitutionDossierHtml(report)
: renderMunicipalityDossierHtml(report)
export const renderDossierMd = (report) =>
isInstitutionUnit(report.unit)
? renderInstitutionDossierMd(report)
: renderMunicipalityDossierMd(report)