/** * 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) => `

Dossiê de atuação pública · ${htmlEscape(report.meta.municipalityName)}

${htmlEscape(title)}

${subtitle ? `

${htmlEscape(subtitle)}

` : ''}

Gerado em ${htmlEscape(formatDateTimeBr(report.meta.generatedAt))}

INSUMO INTERNO

pág. ${pageNo}/${pageCount(report)}

` 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'])}

${htmlEscape(report.cover.scope)}

${htmlEscape(report.cover.version)}

` 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.remaining > 0 ? `

${htmlEscape(moreItemsLabel(hooks.remaining, 'gancho', 'ganchos'))} nas eras.

` : ''}
` : '
' } ${ pending.items.length ? `

04 · o que falta

${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)}

${ era.numbers.items.length ? `

Números com fase

Recursos e entregas quantificáveis

${era.numbers.items .map( (row) => ``, ) .join('')}
Valores por ano, fase, esfera e fonte
ObjetoValorAnoFaseEsferaFonte
${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' : ''}
${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')}
${report.region.evidence.items .map( (item) => ``, ) .join('')}
Itens do recorte regional com evidência e fonte
ItemEsferaEvidência de alcanceFonte
${copyHtml(briefOr(item.brief, item.item))} ${scopeBadge(item.sphere)} ${copyHtml(noteOr(item.brief, item.evidence))} ${sourceLink(item.sourceUrl)}
${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

${rows}
Tabela de lacunas explícitas e próximos passos de apuração
LacunaSituação da buscaLimite editorialPróximo passo
${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

${chunk .map( (row) => ``, ) .join('')}
Fontes consultadas com largura fixa: data 12%, veículo 13%, título 47% e link 28%
DataVeículoTítuloLink
${htmlEscape(formatShortDateBr(row.date))} ${htmlEscape(row.outlet)} ${htmlEscape(row.title)} ${sourceLink(row.url, '(fonte)')}
${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('')}
${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) => `

Dossiê institucional · ${htmlEscape(report.meta.subjectName)}

${htmlEscape(title)}

${subtitle ? `

${htmlEscape(subtitle)}

` : ''}

INSUMO INTERNO — defeso 2026

Gerado em ${htmlEscape(formatDateTimeBr(report.meta.generatedAt))}

pág. ${pageNo}/${report.meta.pageTotal}

` 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'])}

${htmlEscape(report.cover.scope)}

Documento de trabalho
versão 01

` 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

` : '
' } ${ pending.items.length ? `

O que falta

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)}

${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)}

${ era.numbers.items.length ? `

Entregas localizadas

Objeto, valor, fase e alcance

${era.numbers.items .map( (row) => ``, ) .join('')}
Valores por ano, fase, abrangência e fonte
ObjetoValorAnoFaseAbrangênciaFonte
${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' : ''}

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

${reach.evidence.items .map( (row) => ``, ) .join('')}
Itens de setor e rede com evidência de alcance
ItemAbrangênciaEvidência de alcanceFonte
${copyHtml(briefOr(row.brief, row.item))} ${scopeBadge(row.sphere, null, report.unit)} ${copyHtml(row.evidence)} ${sourceLink(row.sourceUrl)}
${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

${honors .map( (honor) => ``, ) .join('')}
Títulos, honrarias e vínculos por data e fonte
DataNaturezaRegistroFonte
${htmlEscape(honorDate(honor))} reconhecimento ${copyHtml(briefOr(honor.brief, honor.text))}${noteOr(honor.brief, honor.detail) ? `
${copyHtml(noteOr(honor.brief, honor.detail))}` : ''}
${sourceLink(honor.sourceUrl)}

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

${rows}
Tabela de lacunas explícitas e próximos passos de apuração
LacunaSituação da buscaLimite editorialPróximo passo
${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

${report.news .map( (row) => ``, ) .join('')}
Fontes consultadas: data 12%, veículo 13%, título 47% e link 28%
DataVeículoTítuloLink
${htmlEscape(formatShortDateBr(row.date))} ${htmlEscape(row.outlet)} ${htmlEscape(row.title)} ${sourceLink(row.url, '(fonte)')}
` : '' } ${ report.acervo?.items?.length ? `

Acervo interno (read-only)

Falas do mandato sobre o recorte

Base Teqo, 2011+

${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('')}
${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)