/** * 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, formatMoneyCompact } 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('
')}

` /** Total of sheets rendered so far — the shell's `pág. N/M` reads it. */ const pageCount = (report) => report.meta.pageTotal 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)}
` /** Region cards (fixed sheet): counts per recorte, no combined total. */ 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.

` : '' }

Leitura lado a lado

Duas listas, dois totais, nenhuma soma

${scopeBadge('municipio', null, report.unit)}${report.region.municipal.total} ${report.region.municipal.total === 1 ? 'item' : 'itens'} com fonte · DIRETO

lista completa nas páginas seguintes

${scopeBadge('regiao', 'região / polo', report.unit)}SEM TOTAL COMBINADO · ${report.region.regional.total} ${report.region.regional.total === 1 ? 'item' : 'itens'}

lista completa nas páginas seguintes

${ 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)}
` /** Full lists of the region recorte, uncapped: each one flows across sheets. */ const regionListUnits = (report, list, probe) => list.items.map((item, index) => ({ layout: 'tr', html: ` ${copyHtml(briefOr(item.brief, item.answer))} ${scopeBadge(item.sphere, null, report.unit)} ${copyHtml(item.details ?? item.brief?.note ?? '—')} ${sourceLink(item.sourceUrl)} `, })) const renderRegionListSheet = ({ report, list, entry, probe, pageNo }) => `
${sheetHeader( report, `Região / polo · ${list.label}`, entry.index === 0 ? `${list.total} ${list.total === 1 ? 'item' : 'itens'} com fonte` : `continuação ${entry.index + 1} · ${list.total} ${list.total === 1 ? 'item' : 'itens'} com fonte`, pageNo, )} ${ entry.index === 0 ? `
${WARNING_ICON}

${htmlEscape(report.region.ruleTitle)}

Cada linha informa sua esfera; região e polo não são somados ao município.

` : '' }
${tableHead(['Item', 'Esfera', 'Evidência de alcance', 'Fonte'])} ${entry.chunk.map((unit) => unit.html).join('')}
Itens do recorte com evidência de alcance e fonte
${sheetFooter(report, `Região/polo · ${list.label}`, pageNo)}
` const renderRegionListSheets = ({ report, key, anchor, list, pack, probe, nextPage }) => { if (list.items.length === 0) return [] const units = regionListUnits(report, list, probe) return packedSection({ key, anchor, units, pack, probe }).map((entry) => renderRegionListSheet({ report, list, entry, probe, pageNo: nextPage() }), ) } 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; } .chart-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 3mm; margin-top: 3mm; } .chart { display: block; width: 100%; height: auto; } .chart-card { border: .25mm solid #cbd5dc; border-radius: .25rem; padding: 3mm; break-inside: avoid; } .chart-title { margin: 0 0 2mm; font-size: 9.5pt; font-weight: 600; } .chart-label { font-size: 7.4pt; fill: #435264; } .chart-value { font-size: 7.6pt; fill: #1f2933; font-weight: 700; } .chart-legend { margin: 1.5mm 0 0; font-size: 7.6pt; line-height: 1.3; color: #435264; } .synthesis-lines { margin: 0; padding-left: 4.5mm; font-size: 9.5pt; line-height: 1.35; } .synthesis-lines li { margin: 0 0 1.8mm; } .synthesis-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 3mm; margin-top: 3mm; } .opening-paragraph { margin: 0 0 3.4mm; font-size: 11pt; line-height: 1.45; } .opening-paragraph:last-child { margin-bottom: 0; } .era-summary { margin-top: 3.4mm; border-left: 1.2mm solid #315c75; background: #f7fafb; padding: 2.6mm 3mm; } .era-summary p { margin: 0; font-size: 10pt; line-height: 1.4; } .value-list { list-style: none; margin: 0; padding: 0; } .value-list li { display: grid; grid-template-columns: 1fr 20mm 18mm; gap: 1.6mm; align-items: center; padding: 0.9mm 0; border-bottom: .25mm solid #e3e9ec; font-size: 8.2pt; line-height: 1.22; } .value-list li:last-child { border-bottom: 0; } .value-list-bar { display: block; height: 2.2mm; background: #eef2f5; border-radius: 1mm; overflow: hidden; } .value-list-bar span { display: block; height: 100%; background: #315c75; } .value-list-value { text-align: right; font-weight: 700; font-variant-numeric: tabular-nums; white-space: nowrap; } ` const renderMunicipalityDossierHtml = (report, { pack = null, probe = false } = {}) => { const buildParts = () => { let page = 0 const nextPage = () => ++page const parts = [] parts.push(renderCover(report)) page += 1 const opening = renderUnitOpeningPage(report, page + 1) if (opening) { parts.push(opening) page += 1 } parts.push(renderTrajectory(report, nextPage())) parts.push(renderSummary(report, nextPage())) parts.push(renderUnitSynthesisPage(report, nextPage())) parts.push(renderUnitChartsPage(report, nextPage())) for (const era of report.eras) { parts.push(...renderEraSheets({ report, era, pack, probe, nextPage, includeHonors: true })) } parts.push(renderRegion(report, nextPage())) parts.push( ...renderRegionListSheets({ report, key: 'region:municipio', anchor: 'regiao-municipio', list: report.region.municipal, pack, probe, nextPage, }), ) parts.push( ...renderRegionListSheets({ report, key: 'region:regional', anchor: 'regiao-regional', list: report.region.regional, pack, probe, nextPage, }), ) parts.push(...renderGapSheets({ report, pack, probe, nextPage })) parts.push(...renderNewsSheets({ report, pack, probe, nextPage })) parts.push(...renderAcervoSheets({ report, pack, probe, nextPage })) parts.push(renderUnitSourcesPage(report, nextPage())) return parts } // Two passes: the shell's `pág. N/M` needs the final sheet count, and the // count only exists after the pack plan is applied. report.meta.pageTotal = 0 let parts = buildParts() report.meta.pageTotal = parts.length parts = buildParts() 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.narrative || era.summary) { lines.push('', `> ${era.narrative ?? era.summary}`) } if (listOf(era.numbers).length) { lines.push( '', '| Objeto | Valor | Ano | Fase | Esfera | Fonte |', '| --- | --- | --- | --- | --- | --- |', ) for (const row of listOf(era.numbers)) { lines.push( `| ${row.object} | ${row.value ?? '—'} | ${row.year ?? '—'} | ${dossierPhaseLabel(row.phase)} | ${row.sphere} | ${row.sourceUrl ?? '—'} |`, ) } } for (const action of listOf(era.actions)) { lines.push( '', `- **${stripInlineSources(action.title)}** (${action.sphere}${action.year ? ` · ${action.year}` : ''})${action.detail ? ` — ${stripInlineSources(action.detail)}` : ''} [fonte](${action.sourceUrl})`, ) } for (const honor of listOf(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.', '', ] if (report.opening?.paragraphs?.length) { lines.push(`## ${report.opening.title}`, '') for (const paragraph of report.opening.paragraphs) lines.push(paragraph, '') lines.push( `_Redação de síntese sobre os pontos com fonte; ${ report.opening.authored ? 'escrita a partir dos itens datados do dossiê' : 'repetição da leitura dos números (sem redação autoral disponível)' } — nada aqui acrescenta fato novo._`, '', ) } lines.push('## 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)) } if (report.synthesis?.lines?.length) { lines.push('', '## Síntese do que foi localizado', '') for (const line of report.synthesis.lines) lines.push(`- ${line}`) lines.push('', '### Números por recorte', '') lines.push('| Era | Município | Região/polo | Total |', '| --- | --- | --- | --- |') for (const row of report.synthesis.byEraSphere) { const countFor = (key) => row.segments.find((segment) => segment.key === key)?.count ?? 0 lines.push( `| ${row.label} | ${countFor('municipio')} | ${countFor('regiao') + countFor('polo')} | ${row.segments.reduce((sum, segment) => sum + segment.count, 0)} |`, ) } if (report.synthesis.moneyByPhase.length) { lines.push('', '### Recursos por fase', '') for (const row of report.synthesis.moneyByPhase) { lines.push(`- ${row.label}: ${row.count} · ${formatMoneyCompact(row.amount)}`) } } if (report.synthesis.byArea.length) { lines.push('', '### Temas com mais registros', '') for (const row of report.synthesis.byArea) lines.push(`- ${row.key}: ${row.count}`) } } 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(briefOr(item.brief, item.answer))}** (${item.sphere})${item.details ? ` — ${stripInlineSources(item.details)}` : ''} [fonte](${item.sourceUrl})`, ) } lines.push('', '### Região / polo (não somável)', '') for (const item of report.region.regional.items) { lines.push( `- **${stripInlineSources(briefOr(item.brief, item.answer))}** (${item.sphere})${item.details ? ` — ${stripInlineSources(item.details)}` : ''} [fonte](${item.sourceUrl})`, ) } if (report.acervo?.items?.length) { lines.push( '', '## Acervo interno (read-only)', '', `_Amostra de ${report.acervo.items.length} de ${report.acervo.recorte} falas do recorte, com link; o acervo completo fica na base Teqo. ${report.unit?.acervoNote ?? ''}_`, '', ) for (const row of report.acervo.items) { lines.push( `- **${row.period}** — ${stripInlineSources(row.text)}${row.excerpt ? ` _(${stripInlineSources(row.excerpt)})_` : ''} [fonte](${row.sourceUrl})`, ) } } lines.push('', '## Lacunas explícitas', '') if (report.gaps.length === 0) lines.push('_Nenhuma lacuna declarada._') for (const gap of report.gaps) { lines.push(`- **${gap.label}:** ${gap.reason} → ${gap.nextStep}`) } if (report.news.length) { lines.push('', '## Notícias e documentos consultados', '') for (const row of report.news) { lines.push(`- ${formatDateBr(row.date)} · ${row.outlet} — ${row.title} — ${row.url}`) } } lines.push( '', '## Limites de cobertura', '', ...report.limits.coverage.map((item) => `- ${item}`), '', '## Regras para uso editorial', '', ...report.limits.editorial.map((item) => `- ${item}`), '', '## Nota de defeso eleitoral 2026', '', 'Insumo interno não é autorização para publicar. Publicidade institucional e propaganda eleitoral obedecem a regimes diferentes. Antes de adaptar qualquer trecho para peça externa, submeta texto, imagem, autoria, período e canal à revisão responsável.', '', `_Documento interno de campanha, datado (${formatDateTimeBr(report.meta.generatedAt)}). O dado envelhece: confira a data de leitura da base._`, ) return `${lines.join('\n')}\n` } /* ------------------------------------------------------------------ * * 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; } ` /** * Anchor → pack-section key of the flowing sheets. The builder maps an * overflowing sheet back to its pack entry and shrinks that chunk (fallback of * the measured pack plan). */ const MUNICIPALITY_PACK_ANCHORS = { 'era-a': 'era:A', 'era-b': 'era:B', 'era-c': 'era:C', 'regiao-municipio': 'region:municipio', 'regiao-regional': 'region:regional', lacunas: 'gaps', noticias: 'news', acervo: 'acervo', } export const INSTITUTION_PACK_ANCHORS = { 'era-a': 'era:A', 'era-b': 'era:B', 'era-c': 'era:C', titulos: 'honors', 'abrangencia-instituicao': 'scope:instituicao', 'abrangencia-setor': 'scope:setor', 'abrangencia-rede': 'scope:rede', lacunas: 'gaps', noticias: 'news', acervo: 'acervo', } /** * Probe-only CSS: the packed sections measure their true content height, with * no page clamp and no inter-row gaps (the packer adds the card-row gap back). */ const INSTITUTION_PROBE_CSS = ` .sheet { height: auto; min-height: 0; overflow: visible; } [data-pack-section] .block, [data-pack-section] .action-grid, [data-pack-section] .document-table, [data-pack-section] .tight-list, [data-pack-section] .era-method, [data-pack-section] .page1-grid { margin-top: 0; } [data-pack-section] .action-grid, [data-pack-section] .era-method { gap: 0; } [data-pack-section] .tight-list li { margin-bottom: 0; } ` 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}

` /** Shared sheets (síntese, gráficos, carta, eras correntes) use the unit's shell. */ const unitHeader = (report, title, subtitle, pageNo) => isInstitutionUnit(report.unit) ? instHeader(report, title, subtitle, pageNo) : sheetHeader(report, title, subtitle, pageNo) const unitFooter = (report, left, pageNo) => isInstitutionUnit(report.unit) ? instFooter(report, left, pageNo) : sheetFooter(report, left, 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', hint = 'com fonte fora desta página — nada foi descartado.', ) => { if (cappedList.omitted <= 0) return '' const label = cappedList.omitted === 1 ? (CAPPED_NOUNS[noun] ?? noun) : noun return `

e mais ${cappedList.omitted} ${label} ${htmlEscape(hint)}

` } /* ---------------------------------------------------------------- * * Flowing sheets (C187): packed sections render as many sheets as * * the content needs — nothing is capped. In probe mode a section * * renders one sheet with every unit tagged (`data-pack-unit`), so * * the builder measures real heights and returns a pack plan. * * ---------------------------------------------------------------- */ const splitUnits = (units, sizes) => { const chunks = [] let offset = 0 for (const size of sizes) { chunks.push(units.slice(offset, offset + size)) offset += size } if (offset < units.length) chunks.push(units.slice(offset)) return chunks } /** Sheet plans of one section: `[{ key, anchor, chunk, index }]`. */ const packedSection = ({ key, anchor, units, pack, probe }) => { const sizes = probe ? null : pack?.[key] const chunks = sizes ? splitUnits(units, sizes) : [units] return chunks.map((chunk, index) => ({ key, chunk, index, anchor: index === 0 ? anchor : `${anchor}-${index + 1}`, })) } const unitAttrs = (probe, index, layout) => probe ? ` data-pack-unit="${index}" data-pack-layout="${layout}"` : '' const groupUnits = (chunk) => { const groups = [] for (const unit of chunk) { const last = groups[groups.length - 1] if (last && last.layout === unit.layout) last.units.push(unit) else groups.push({ layout: unit.layout, units: [unit] }) } return groups } const tableHead = (columns) => `${columns.map((column) => `${htmlEscape(column)}`).join('')}` /* ---------------------------------------------------------------- * * Synthesis charts (inline SVG, print-safe): aggregations of the * * sourced rows only — never a new fact, never a combined total * * across abrangências. * * ---------------------------------------------------------------- */ const CHART_COLORS = { municipio: '#315c75', regiao: '#9d8b64', polo: '#8f819c', instituicao: '#315c75', setor: '#9d8b64', rede: '#8f819c', empenhado: '#6b4918', liquidado: '#4a6b7c', pago: '#285338', autorizado: '#8a5a18', restos: '#5b6470', nao_informado: '#8b96a2', default: '#315c75', } const barChart = ({ rows, width = 330, rowHeight = 19, labelWidth = 118 }) => { const max = Math.max(1, ...rows.map((row) => row.count)) const barMax = width - labelWidth - 26 const height = rows.length * rowHeight + 4 return ` ${rows .map((row, index) => { const y = index * rowHeight + 3 const barWidth = Math.max(2, Math.round((row.count / max) * barMax)) return `${htmlEscape(row.label)} ${row.count}` }) .join('')} ` } /** HTML list with a proportional bar per row — long labels wrap, nothing overlaps. */ const valueList = ({ rows, format }) => { const max = Math.max(1, ...rows.map((row) => row.value)) return `` } /** Vertical columns with the value label above and the year label below. */ const columnChart = ({ rows, width = 330, height = 132, format = (row) => String(row.count) }) => { const max = Math.max(1, ...rows.map((row) => row.value ?? row.count)) const baseY = height - 18 const plotHeight = baseY - 16 const columnWidth = (width - 8) / Math.max(1, rows.length) return ` ${rows .map((row, index) => { const value = row.value ?? row.count const barHeight = Math.max(2, Math.round((value / max) * plotHeight)) const x = 4 + index * columnWidth const y = baseY - barHeight return ` ${htmlEscape(format(row))} ${htmlEscape(String(row.key).slice(-2))}` }) .join('')} ` } /** Vertical columns stacked by phase (money by year), total label above. */ const stackedColumnChart = ({ rows, width = 330, height = 132, format }) => { const max = Math.max(1, ...rows.map((row) => row.value)) const baseY = height - 18 const plotHeight = baseY - 16 const columnWidth = (width - 8) / Math.max(1, rows.length) return ` ${rows .map((row, index) => { const x = 4 + index * columnWidth let y = baseY const bars = row.segments .map((segment) => { const segmentHeight = Math.max(1, Math.round((segment.amount / max) * plotHeight)) y -= segmentHeight return `` }) .join('') const top = baseY - Math.round((row.value / max) * plotHeight) return `${bars} ${htmlEscape(format(row))} ${htmlEscape(String(row.key).slice(-2))}` }) .join('')} ` } 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', 'com fonte — lista completa nas seções por era.')}` } 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', 'com fonte — lista completa nas seções por era e de abrangência.')}

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 renderEraEmptySheet = (report, era, pageNo) => `
${unitHeader(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.
${unitFooter(report, `${era.label} · lacuna explícita`, pageNo)}
` /** Accepts the institution's capped objects and the city's plain arrays. */ const listOf = (value) => (Array.isArray(value) ? value : (value?.items ?? [])) /** * Era units: value rows (table), action cards (2-col grid) and — city only, which * keeps honors in the era sheet — the honor list items. Honors have their own * sheet in the institution dossiê, so `includeHonors` stays false there. */ const eraUnits = (report, era, probe, { includeHonors = false } = {}) => { const unit = report.unit ?? MUNICIPALITY_UNIT const units = [] for (const row of listOf(era.numbers)) { units.push({ layout: 'tr', html: ` ${htmlEscape(row.object)} ${valueCell(row.value, 'Valor a recuperar')} ${htmlEscape(row.year ?? '—')} ${phaseBadge(row.phase)} ${scopeBadge(row.sphere, null, unit)} ${sourceLink(row.sourceUrl)}${ row.sphere !== unit.defaultSphere ? `
${htmlEscape(unit.notSummedInline ?? 'não somar')}` : '' } `, }) } for (const action of listOf(era.actions)) { units.push({ layout: 'card', html: `
${scopeBadge(action.sphere, null, unit)}${htmlEscape(action.year ?? '—')}

${copyHtml(briefOr(action.brief, action.title))}

${noteOr(action.brief, action.detail) ? `

${copyHtml(noteOr(action.brief, action.detail))}

` : ''} ${sourceLink(action.sourceUrl)}
`, }) } if (includeHonors) { for (const honor of listOf(era.honors)) { units.push({ layout: 'li', html: `${copyHtml(briefOr(honor.brief, honor.text))} ${sourceLink(honor.sourceUrl)}`, }) } } return units } const eraNumbersTable = (units, unit) => ` ${tableHead(['Objeto', 'Valor', 'Ano', 'Fase', unit.sphereColumnLabel ?? 'Esfera', 'Fonte'])} ${units.map((entry) => entry.html).join('')}
Valores por ano, fase, esfera e fonte
` const eraHonorsPanel = (units, unit) => `

${htmlEscape(unit.honorsTitle ?? 'Títulos, honrarias e vínculos')}

    ${units.map((entry) => entry.html).join('')}
${assetBox('NEEDS ASSET', ['reprodução do ato ou clipping', 'somente com origem e licença'])}
` const renderEraSheet = ({ report, era, entry, probe, pageNo }) => { const unit = report.unit ?? MUNICIPALITY_UNIT const body = groupUnits(entry.chunk) .map((group) => { if (group.layout === 'tr') { return `

Entregas localizadas

${htmlEscape(unit.eraNumbersTitle ?? 'Objeto, valor, fase e alcance')}

${eraNumbersTable(group.units, unit)}

Não consolidar fases como se fossem equivalentes. Um valor empenhado não é um valor pago.

` } if (group.layout === 'li') return eraHonorsPanel(group.units, unit) return `

Atuação registrada

${htmlEscape(unit.eraActionsTitle ?? 'Papéis com evidência visível')}

${group.units.map((item) => item.html).join('')}
` }) .join('') return `
${unitHeader(report, era.label, entry.index === 0 ? `${era.period} · ${era.subtitle}` : `continuação ${entry.index + 1} · ${era.period}`, pageNo)} ${ entry.index === 0 ? `

Recorte e método

${htmlEscape(era.method)}

` : '' } ${ entry.index === 0 && (era.narrative || era.summary) ? `

O que esta era entrega

${copyHtml(era.narrative ?? era.summary)}

` : '' } ${body} ${unitFooter(report, era.label, pageNo)}
` } const renderEraSheets = ({ report, era, pack, probe, nextPage, includeHonors = false }) => { if (era.empty) return [renderEraEmptySheet(report, era, nextPage())] const units = eraUnits(report, era, probe, { includeHonors }) return packedSection({ key: `era:${era.id}`, anchor: `era-${era.id.toLowerCase()}`, units, pack, probe, }).map((entry) => renderEraSheet({ report, era, entry, probe, pageNo: nextPage() })) } /** Anchor map of the unit's packed sections (the builder inverts it). */ export const dossierPackAnchors = (unit) => isInstitutionUnit(unit) ? INSTITUTION_PACK_ANCHORS : MUNICIPALITY_PACK_ANCHORS const INSTITUTION_SCOPE_WORDS = { institution: 'instituicao', sector: 'setor', network: 'rede', } const listYearRange = (items) => { const years = items .map((item) => item.year) .filter(Boolean) .sort() return years.length ? `${years[0]}–${years[years.length - 1]}` : null } 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${listYearRange(list.items) ? ` · ${listYearRange(list.items)}` : ''} · lista completa nas páginas seguintes

    ${list.items .slice(0, 2) .map( (item) => `
  • ${copyHtml(briefOr(item.brief, item.title))} ${sourceLink(item.sourceUrl)}
  • `, ) .join( '', )}${list.items.length === 0 ? '
  • Nenhum item com fonte.
  • ' : ''}
`, ) .join('')}
${ 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)}
` } /** Sphere lists are uncapped: each one flows across as many sheets as it needs. */ const institutionScopeUnits = (report, list, probe) => list.items.map((item, index) => ({ layout: 'tr', html: ` ${copyHtml(briefOr(item.brief, item.title))} ${scopeBadge(item.sphere, null, report.unit)} ${copyHtml(noteOr(item.brief, item.evidence))} ${sourceLink(item.sourceUrl)} `, })) const renderInstitutionScopeListSheet = ({ report, list, entry, probe, pageNo }) => { const word = INSTITUTION_SCOPE_WORDS[list.key] ?? list.key const notSummable = list.sphere !== 'instituicao' return `
${instHeader( report, `Abrangência · ${list.label}`, entry.index === 0 ? `${list.total} ${list.total === 1 ? 'item' : 'itens'} com fonte · ${notSummable ? 'não somar à instituição' : 'recorte direto'}` : `continuação ${entry.index + 1} · ${list.total} ${list.total === 1 ? 'item' : 'itens'} com fonte`, pageNo, )} ${ entry.index === 0 ? `
${WARNING_ICON}

${htmlEscape(report.reach.ruleTitle)}

Cada linha informa sua abrangência; setor e rede não são somados à instituição.

` : '' }
${tableHead(['Item', 'Abrangência', 'Evidência de alcance', 'Fonte'])} ${entry.chunk.map((unit) => unit.html).join('')}
Itens do recorte com evidência de alcance e fonte
${instFooter(report, `Abrangência · ${list.label}`, pageNo)}
` } const renderInstitutionScopeListSheets = ({ report, list, pack, probe, nextPage }) => { if (list.items.length === 0) return [] const word = INSTITUTION_SCOPE_WORDS[list.key] ?? list.key const units = institutionScopeUnits(report, list, probe) return packedSection({ key: `scope:${word}`, anchor: `abrangencia-${word}`, units, pack, probe, }).map((entry) => renderInstitutionScopeListSheet({ report, list, entry, probe, pageNo: nextPage() }), ) } 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?.items ?? [] const institutionHonorUnits = (probe) => institutionHonors(probe.report).map((honor, index) => ({ layout: 'tr', html: ` ${htmlEscape(honorDate(honor))} reconhecimento ${copyHtml(briefOr(honor.brief, honor.text))}${noteOr(honor.brief, honor.detail) ? `
${copyHtml(noteOr(honor.brief, honor.detail))}` : ''} ${sourceLink(honor.sourceUrl)} `, })) const renderInstitutionHonorSheet = ({ report, entry, probe, pageNo }) => { 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', entry.index === 0 ? 'Reconhecimento não substitui entrega; vínculo não prova resultado' : `continuação ${entry.index + 1}`, pageNo, )}

Registros confirmados

Data, natureza e fonte primária

${tableHead(['Data', 'Natureza', 'Registro', 'Fonte'])} ${entry.chunk.map((unit) => unit.html).join('')}
Títulos, honrarias e vínculos por data e fonte
${ entry.index === 0 ? `

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 renderInstitutionHonorSheets = ({ report, pack, probe, nextPage }) => { const units = institutionHonorUnits({ report, probe }) if (units.length === 0) return [] return packedSection({ key: 'honors', anchor: 'titulos', units, pack, probe }).map((entry) => renderInstitutionHonorSheet({ report, entry, probe, pageNo: nextPage() }), ) } /** * Gaps and news are long full-text tables: packed in continuation sheets, so * the sources sheet keeps only the acervo, limits and defeso. */ const gapUnits = (report, probe) => report.gaps.map((gap, index) => ({ layout: 'tr', html: ` ${htmlEscape(gap.label)} ${htmlEscape(gap.reason)} Não completar por inferência. ${htmlEscape(gap.nextStep)} `, })) const renderGapSheet = ({ report, entry, probe, pageNo }) => `
${unitHeader(report, 'Lacunas explícitas', `O que não foi encontrado${entry.index > 0 ? ` · continuação ${entry.index + 1}` : ''}`, pageNo)}

O que não foi encontrado

Lacunas explícitas

Não completar por memória ou inferência

${tableHead(['Lacuna', 'Situação da busca', 'Limite editorial', 'Próximo passo'])} ${ entry.chunk.length ? entry.chunk.map((unit) => unit.html).join('') : '' }
Tabela de lacunas explícitas e próximos passos de apuração
Nenhuma lacuna declarada — confira a cobertura das eras.
${unitFooter(report, 'Lacunas explícitas · INSUMO INTERNO', pageNo)}
` const renderGapSheets = ({ report, pack, probe, nextPage }) => { const units = gapUnits(report, probe) if (units.length === 0) { return [ renderGapSheet({ report, entry: { anchor: 'lacunas', chunk: [], index: 0 }, probe: false, pageNo: nextPage(), }), ] } return packedSection({ key: 'gaps', anchor: 'lacunas', units, pack, probe }).map((entry) => renderGapSheet({ report, entry, probe, pageNo: nextPage() }), ) } const newsUnits = (report, probe) => report.news.map((row, index) => ({ layout: 'tr', html: ` ${htmlEscape(formatShortDateBr(row.date))} ${htmlEscape(row.outlet)} ${htmlEscape(row.title)} ${sourceLink(row.url, '(fonte)')} `, })) const renderNewsSheet = ({ report, entry, probe, pageNo }) => `
${unitHeader(report, 'Notícias e documentos consultados', `Fontes externas datadas${entry.index > 0 ? ` · continuação ${entry.index + 1}` : ''}`, pageNo)}

Lista clicável

Notícias e documentos consultados

${tableHead(['Data', 'Veículo', 'Título', 'Link'])} ${entry.chunk.map((unit) => unit.html).join('')}
Fontes consultadas: data 12%, veículo 13%, título 47% e link 28%
${unitFooter(report, 'Notícias e documentos · INSUMO INTERNO', pageNo)}
` const renderNewsSheets = ({ report, pack, probe, nextPage }) => { const units = newsUnits(report, probe) if (units.length === 0) return [] return packedSection({ key: 'news', anchor: 'noticias', units, pack, probe }).map((entry) => renderNewsSheet({ report, entry, probe, pageNo: nextPage() }), ) } /** Acervo sample (source panel): declared sample with the total carried by the header. */ const acervoUnits = (report, probe) => report.acervo.items.map((row, index) => ({ layout: 'tr', html: `${htmlEscape(row.period)} — ${copyHtml(row.text)} ${sourceLink(row.sourceUrl)}`, })) const renderAcervoSheet = ({ report, entry, probe, pageNo, isLast }) => `
${unitHeader( report, 'Acervo interno de falas (amostra)', entry.index === 0 ? `Base Teqo 2011+ · ${report.acervo.items.length} de ${report.acervo.recorte} falas do recorte, com link · ${htmlEscape(report.unit.acervoNote ?? '')}` : `continuação ${entry.index + 1}`, pageNo, )}

Falas do mandato sobre o recorte

Amostra com link direto

    ${entry.chunk.map((unit) => unit.html).join('')}
${ isLast ? `

Amostra das falas mais recentes do recorte (${report.acervo.items.length} de ${report.acervo.recorte}); o acervo completo fica na base Teqo. ${htmlEscape(report.unit.acervoNote ?? '')}

` : '' }
${unitFooter(report, 'Acervo interno · amostra', pageNo)}
` const renderAcervoSheets = ({ report, pack, probe, nextPage }) => { const units = acervoUnits(report, probe) if (units.length === 0) return [] const entries = packedSection({ key: 'acervo', anchor: 'acervo', units, pack, probe }) return entries.map((entry, index) => renderAcervoSheet({ report, entry, probe, pageNo: nextPage(), isLast: index === entries.length - 1, }), ) } const renderUnitSourcesPage = (report, pageNo) => `
${unitHeader(report, 'Fontes e limites', 'O que foi consultado e o que não se pode afirmar', pageNo)} ${ report.acervo?.recorte ? `
${WARNING_ICON}

Acervo interno: ${report.acervo.recorte} falas no recorte.

A amostra com link (${report.acervo.items.length} de ${report.acervo.recorte}) está na seção anterior; o acervo completo fica na base Teqo. ${htmlEscape(report.unit.acervoNote ?? '')}

` : '' }

Limites de cobertura

    ${report.limits.coverage .map((item) => `
  • ${htmlEscape(item)}
  • `) .join('')}

Regras para uso editorial

    ${report.limits.editorial .map((item) => `
  • ${htmlEscape(item)}
  • `) .join('')}
${unitFooter(report, 'Fontes e limites · INSUMO INTERNO', pageNo)}
` const renderUnitSynthesisPage = (report, pageNo) => `
${unitHeader(report, 'Síntese do que foi localizado', 'Leitura dos números com lastro — contagens por recorte, nunca soma entre eles', pageNo)}

Leitura rápida

O que os pontos com fonte mostram

    ${report.synthesis.lines.map((line) => `
  • ${htmlEscape(line)}
  • `).join('')}

Cobertura das eras

    ${report.eras .map( (era) => `
  • ${htmlEscape(era.label)} — ${ era.empty ? 'sem evidência suficiente (lacuna explícita)' : `${listOf(era.numbers).length + listOf(era.actions).length} pontos nesta seção` }
  • `, ) .join('')}

Guardas de leitura

  • Contagens por recorte: setor e rede nunca somados à instituição.
  • Cada valor mantém a fase — empenho ≠ pagamento.
  • Sem fonte, não publica: ausência vira lacuna explícita.
${ report.synthesis.moneyByPhase.length ? `

Recursos por fase

Valores localizados, sem consolidar fases

${tableHead(['Fase', 'Itens', 'Valor'])} ${report.synthesis.moneyByPhase .map( (row) => ``, ) .join('')}
Valores localizados por fase de execução
${phaseBadge(row.key)}${row.count}${htmlEscape(formatMoneyCompact(row.amount))}

Soma por fase apenas; fases não são equivalentes entre si.

` : '' } ${unitFooter(report, 'Síntese · INSUMO INTERNO', pageNo)}
` const renderUnitChartsPage = (report, pageNo) => { const synthesis = report.synthesis const executedPhases = new Set(['pago', 'liquidado', 'autorizado', 'empenhado']) const executedYears = synthesis.moneyByYear.filter((row) => row.segments.some((segment) => executedPhases.has(segment.key)), ) return `
${unitHeader(report, 'Gráficos consolidados', 'Como os recursos e as entregas chegaram à instituição — valores sempre com a fase', pageNo)}

Recursos com execução por ano (R$)

${ executedYears.length ? stackedColumnChart({ rows: executedYears.map((row) => ({ ...row, value: row.amount })), height: 136, format: (row) => formatMoneyCompact(row.value), }) : '

Nenhum valor com fase de execução informada.

' }

verde: pago/liquidado · âmbar: autorizado/empenhado — empenho não é pagamento.

Propostas e articulações sem fase informada (R$)

${ synthesis.moneyProposals.length ? valueList({ rows: synthesis.moneyProposals.map((row) => ({ label: `${row.year ?? '—'} · ${row.label}`, value: row.amount, })), format: (row) => formatMoneyCompact(row.value), }) : '

Nenhuma proposta sem fase informada.

' }

propostas e convênios citados nas fontes, sem fase de execução declarada.

Abrangência: ${report.unit.spheres .map((sphere) => htmlEscape(report.unit.sphereLabels[sphere])) .join(' × ')}

${barChart({ rows: synthesis.bySphere.map((row) => ({ label: row.label, count: row.count, color: CHART_COLORS[row.key], })), })}

${htmlEscape(report.unit.sumGuardNote ?? '')}

Trajetória: pontos com fonte por ano (20xx)

${columnChart({ rows: synthesis.byYear, height: 156 })}

Áreas com mais registros

${barChart({ rows: synthesis.byArea.slice(0, 6).map((row) => ({ label: row.key, count: row.count })), })}

Lacunas declaradas por era

${barChart({ rows: synthesis.gapsByEra.map((row) => ({ label: row.label, count: row.count })), })}

ausência é resultado: cada lacuna é uma tarefa de apuração.

Acervo interno (read-only)

${synthesis.totals.acervoRecorte}

falas no recorte temático (2011+)

  • Amostra de ${synthesis.totals.acervo} falas com link direto (seção do acervo).
  • Temas: ${report.acervo?.topics?.join(', ') || '—'} — ${htmlEscape(report.unit.acervoNote ?? '')}
${unitFooter(report, 'Gráficos · INSUMO INTERNO', pageNo)}
` } const renderUnitOpeningPage = (report, pageNo) => { const opening = report.opening if (!opening?.paragraphs?.length) return '' return `
${unitHeader(report, opening.title, 'Redação de síntese sobre os pontos com fonte — nada aqui acrescenta fato novo', pageNo)}
${opening.paragraphs.map((paragraph) => `

${copyHtml(paragraph)}

`).join('')}
${unitFooter(report, 'A contribuição · INSUMO INTERNO', pageNo)}
` } const renderInstitutionDossierHtml = (report, { pack = null, probe = false } = {}) => { const buildParts = () => { let page = 0 const nextPage = () => ++page const parts = [] parts.push(renderInstitutionCover(report)) page += 1 const opening = renderUnitOpeningPage(report, page + 1) if (opening) { parts.push(opening) page += 1 } parts.push(renderInstitutionSummary(report, ++page)) parts.push(renderUnitSynthesisPage(report, nextPage())) parts.push(renderUnitChartsPage(report, nextPage())) for (const era of report.eras) { parts.push(...renderEraSheets({ report, era, pack, probe, nextPage, includeHonors: false })) } parts.push(renderInstitutionScope(report, nextPage())) for (const list of report.reach.lists) { parts.push(...renderInstitutionScopeListSheets({ report, list, pack, probe, nextPage })) } parts.push(...renderInstitutionHonorSheets({ report, pack, probe, nextPage })) parts.push(...renderGapSheets({ report, pack, probe, nextPage })) parts.push(...renderNewsSheets({ report, pack, probe, nextPage })) parts.push(...renderAcervoSheets({ report, pack, probe, nextPage })) parts.push(renderUnitSourcesPage(report, nextPage())) return parts } // Two passes: the shell's `pág. N/M` needs the final sheet count, and the // count only exists after the pack plan is applied. report.meta.pageTotal = 0 let parts = buildParts() report.meta.pageTotal = parts.length parts = buildParts() return ` ${htmlEscape(report.meta.title)} — ${htmlEscape(report.meta.subjectName)}${probe ? `` : ''} ${parts.join('\n')} ` } const institutionMdEra = (era) => { const lines = [ `## ${era.label} (${era.period})`, '', `_${era.method}_`, '', `Trilha: ${era.recovery}`, ] if (era.narrative || era.summary) { lines.push('', `> ${era.narrative ?? era.summary}`) } 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 ?? '—'} |`, ) } if (era.numbers.omitted > 0) lines.push('', `_e mais ${era.numbers.omitted} item(ns) de valor com fonte._`) } 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})`, ) } if (era.actions.omitted > 0) lines.push('', `_e mais ${era.actions.omitted} item(ns) de atuação com fonte._`) for (const honor of era.honors.items) lines.push('', `- Honraria: ${stripInlineSources(honor.text)} [fonte](${honor.sourceUrl})`) if (era.honors.omitted > 0) lines.push('', `_e mais ${era.honors.omitted} honraria(s) com fonte._`) 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(' · ') || '—'}`, '', ] if (report.opening?.paragraphs?.length) { lines.push(`## ${report.opening.title}`, '') for (const paragraph of report.opening.paragraphs) lines.push(paragraph, '') lines.push( `_Redação de síntese sobre os pontos com fonte; ${ report.opening.authored ? 'escrita a partir dos itens datados do dossiê' : 'repetição da leitura dos números (sem redação autoral disponível)' } — nada aqui acrescenta fato novo._`, '', ) } lines.push('## 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)) if (report.synthesis?.lines?.length) { lines.push('', '## Síntese do que foi localizado', '') for (const line of report.synthesis.lines) lines.push(`- ${line}`) lines.push('', '### Números por recorte', '') lines.push('| Era | Instituição | Setor | Rede | Total |', '| --- | --- | --- | --- | --- |') for (const row of report.synthesis.byEraSphere) { const countFor = (key) => row.segments.find((segment) => segment.key === key)?.count ?? 0 lines.push( `| ${row.label} | ${countFor('instituicao')} | ${countFor('setor')} | ${countFor('rede')} | ${row.segments.reduce((sum, segment) => sum + segment.count, 0)} |`, ) } if (report.synthesis.byPhase.length) { lines.push('', '### Itens com valor por fase', '') for (const row of report.synthesis.byPhase) lines.push(`- ${row.label}: ${row.count}`) } if (report.synthesis.byArea.length) { lines.push('', '### Temas com mais registros', '') for (const row of report.synthesis.byArea) lines.push(`- ${row.key}: ${row.count}`) } if (report.synthesis.acervoByYear.length) { lines.push('', '### Acervo interno por ano', '') lines.push(report.synthesis.acervoByYear.map((row) => `${row.key}: ${row.count}`).join(' · ')) } } 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(briefOr(item.brief, item.title))}** (${item.sphere}${item.year ? ` · ${item.year}` : ''})${noteOr(item.brief, item.evidence) ? ` — ${stripInlineSources(noteOr(item.brief, item.evidence))}` : ''} [fonte](${item.sourceUrl})`, ) } if (list.omitted > 0) lines.push(`- _e mais ${list.omitted} item(ns) com fonte._`) lines.push('') } if (report.acervo?.items?.length) { lines.push( '', `## Acervo interno (read-only)`, '', `_Amostra de ${report.acervo.items.length} de ${report.acervo.recorte} falas do recorte temático, com link; o acervo completo fica na base Teqo. Recorte por tema, não nominal._`, '', ) 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 na base._`) } 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, options) => isInstitutionUnit(report.unit) ? renderInstitutionDossierHtml(report, options) : renderMunicipalityDossierHtml(report, options) export const renderDossierMd = (report) => isInstitutionUnit(report.unit) ? renderInstitutionDossierMd(report) : renderMunicipalityDossierMd(report)