/** * Dossiê renderer (C186; sober redesign C209): 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 * (`docs/plans/dossies-sobrios-analiticos-ui-design.html`) class-by-class: * analysis first (O essencial, Leitura entre eras, O que Solla defende), lists * and tables as the spine, no cards, colored badges, charts or images. Shared * escaping/source-token contract lives in `reportText.mjs`. */ import { formatDateBr, formatDateTimeBr } from './cityReportFormat.mjs' import { dossierPhaseLabel } from './dossieBlocks.mjs' import { MUNICIPALITY_UNIT, isSubjectUnit, resolveDossierUnit } from './dossieUnit.mjs' import { htmlEscape, stripInlineSources } from './reportText.mjs' const SOURCE_LABEL = '(fonte)' const sourceLink = (url, label = SOURCE_LABEL) => url ? `${htmlEscape(label)}` : '' /** Print copy prefers the reformulated `brief`; the era/MD keep the integral record. */ const copyHtml = (value) => htmlEscape(stripInlineSources(value)) const briefOr = (brief, fallback) => brief?.title ?? fallback const noteOr = (brief, fallback) => brief?.note ?? fallback const phaseText = (phase) => `${htmlEscape(dossierPhaseLabel(phase))}` const placeholder = (label) => `` const valueCell = (value, widthLabel = 'Valor desconhecido') => value === null || value === undefined || value === '' ? placeholder(widthLabel) : `${htmlEscape(value)}` const runningHead = (report, { kicker, title, subtitle, section, pageNo, continuation = null }) => { const docLabel = isSubjectUnit(report.unit) ? report.unit.docLabel : `Dossiê cidade · ${report.meta.municipalityName}` return `

${htmlEscape(kicker ?? docLabel)}

${htmlEscape(title)}

${subtitle ? `

${htmlEscape(subtitle)}

` : ''}
${continuation ? `

${htmlEscape(continuation)}

` : section ? `

${htmlEscape(section)}

` : ''}

${htmlEscape(report.cover.internalNote)}

${htmlEscape(report.meta.generatedAtLabel)} · versão 01

folha ${pageNo}/${report.meta.pageTotal}

` } const runningFooter = (report, { left, anchor, pageNo }) => ` ` const tableHead = (columns) => `${columns.map((column) => `${htmlEscape(column)}`).join('')}` /** * Section numbers of the family: the index order IS the section order. Omitted * eras do not consume a number; the recorte section follows the rendered eras. */ const sectionNumbers = (report) => { const renderedEras = report.eras.filter((era) => !era.omitted) const numbers = { resumo: 1, sintese: 2, trajetoria: 3 } renderedEras.forEach((era, index) => { numbers[`era-${era.id.toLowerCase()}`] = 4 + index }) const afterEras = 4 + renderedEras.length if (isSubjectUnit(report.unit)) { numbers.abrangencia = afterEras numbers.titulos = afterEras + 1 numbers.defende = afterEras + (report.unit.honorsSheet ? 2 : 1) } else { numbers.regiao = afterEras numbers.defende = afterEras + 1 } numbers.lacunas = numbers.defende + 1 return numbers } const sectionLabel = (report, key) => { const number = sectionNumbers(report)[key] return number ? `Seção ${String(number).padStart(2, '0')}` : null } const sectionHead = (eyebrow, title) => `

${htmlEscape(eyebrow)}

${title ? `

${htmlEscape(title)}

` : ''}
` /* ------------------------------------------------------------------ * * Capa (design cena 01) * * ------------------------------------------------------------------ */ const renderCover = (report) => { const cover = report.cover const subject = isSubjectUnit(report.unit) const titleWords = htmlEscape(cover.title) return `

${htmlEscape(cover.eyebrow)}

${htmlEscape(cover.series)}

${htmlEscape(cover.internalNote)}
${htmlEscape(cover.internalNoteSub)}

Jorge Solla · ${htmlEscape(cover.kicker)}

${ subject ? `Dossiê Solla —
${titleWords}` : titleWords.replace(/^(\S+)\s+(.*)$/, '$1 $2') }

${htmlEscape(cover.subtitle)}

${ subject ? report.unit .identityRows(report.meta.identity ?? {}) .map(([key, value]) => `
${htmlEscape(key)}
${htmlEscape(value)}
`) .join('') : `
Território
${htmlEscape(cover.territory)}
Região / polo
${htmlEscape(cover.pole)}
` }
Data
${htmlEscape(report.meta.generatedAtLabel)} · base lida em ${htmlEscape(report.meta.readAtLabel)}
Versão
01 · documento de trabalho · INSUMO INTERNO
Como ler

${htmlEscape(cover.howToUse)}

` } /* ------------------------------------------------------------------ * * O essencial + índice (design cena 02) * * ------------------------------------------------------------------ */ /** * Canonical index of the family: the order the sheets are printed in. An * omitted era stays in the index with the explicit "sem evidência" note — never * a page number. */ const buildIndexEntries = (report) => { const entries = [ { anchor: 'resumo', mdAnchor: 'o-essencial', label: 'O essencial', heading: 'O essencial', included: true, }, { anchor: 'sintese', mdAnchor: 'leitura-entre-eras', label: 'Leitura entre eras', heading: 'Leitura entre eras', included: true, }, { anchor: 'trajetoria', mdAnchor: 'trajetoria-completa', label: 'Trajetória completa', heading: 'Trajetória completa', included: true, }, ] for (const era of report.eras) { entries.push({ anchor: `era-${era.id.toLowerCase()}`, mdAnchor: `era-${era.id.toLowerCase()}`, label: era.label, heading: era.label, included: !era.omitted, }) } if (isSubjectUnit(report.unit)) { entries.push({ anchor: 'abrangencia', mdAnchor: 'abrangencia', label: `Abrangência: ${report.unit.spheres .map((sphere) => report.unit.sphereLabels[sphere]) .join(' × ')}`, heading: 'Abrangência', included: true, }) if (report.unit.honorsSheet) { entries.push({ anchor: 'titulos', mdAnchor: 'titulos', label: 'Títulos, honrarias e vínculos', heading: 'Títulos, honrarias e vínculos', included: report.eras.some((era) => listOf(era.honors).length), }) } } else { entries.push({ anchor: 'regiao', mdAnchor: 'regiao-e-polo', label: 'Região e polo', heading: 'Região / polo', included: true, }) } entries.push({ anchor: 'defende', mdAnchor: 'o-que-solla-defende', label: 'O que Solla defende', heading: 'O que Solla defende', included: true, }) entries.push({ anchor: 'lacunas', mdAnchor: 'lacunas-explicitas', label: 'Lacunas, notícias, acervo e fontes', heading: 'Lacunas explícitas', included: true, }) return entries } const renderIndex = (report, pages, indexMode) => { const entries = buildIndexEntries(report) return `

Índice

    ${(() => { let number = 0 return entries .map((entry) => { const page = pages?.[entry.anchor] const pageLabel = entry.included && page && indexMode === 'pages' ? String(page) : '—' if (!entry.included) { return `
  1. ${htmlEscape(entry.label)} — sem evidência nominal suficiente; consulte as lacunas—
  2. ` } number += 1 return `
  3. ${String(number).padStart(2, '0')}${htmlEscape(entry.label)}${pageLabel}
  4. ` }) .join('') })()}
` } const renderEssentials = (report, { pageNo, pages = null, indexMode = 'pages' }) => { const lede = report.opening.paragraphs return `
${runningHead(report, { kicker: isSubjectUnit(report.unit) ? `${report.unit.docLabel} · ${report.meta.subjectName}` : `Dossiê cidade · ${report.meta.municipalityName}`, title: 'O essencial', subtitle: 'Leitura do recorte antes do inventário', section: sectionLabel(report, 'resumo'), pageNo, })}

Leitura do recorte

${lede.map((paragraph, index) => `

${copyHtml(paragraph)}

`).join('')}
${ isSubjectUnit(report.unit) ? `

Identificação

${report.unit .identityRows(report.meta.identity ?? {}) .map( ([key, value]) => ``, ) .join('')}
Identificação do recorte
${htmlEscape(key)}${htmlEscape(value)}
` : '' }

Fatos-chave

${renderIndex(report, pages, indexMode)} ${runningFooter(report, { left: `${report.meta.subjectName ?? report.meta.municipalityName} · O essencial`, anchor: 'o-essencial', pageNo, })}
` } /* ------------------------------------------------------------------ * * Leitura entre eras (design cena 03) * * ------------------------------------------------------------------ */ const renderBetweenEras = (report, pageNo) => `
${runningHead(report, { kicker: isSubjectUnit(report.unit) ? `${report.unit.docLabel} · ${report.meta.subjectName}` : `Dossiê cidade · ${report.meta.municipalityName}`, title: 'Leitura entre eras', subtitle: 'Concentração, instrumentos, continuidade, ruptura e lacunas', section: sectionLabel(report, 'sintese'), pageNo, })}

O que muda quando os registros são lidos em conjunto

Síntese operacional

${tableHead(['Era', 'Onde está a evidência', 'Como citar com segurança', 'Limite'])} ${report.betweenEras.table .map( (row) => ``, ) .join('')}
Como citar cada era com segurança e qual é o limite
${htmlEscape(row.era)}${htmlEscape(row.where)}${htmlEscape(row.howToCite)}${htmlEscape(row.limit)}
Regra de leitura: análise é orientação para encontrar a evidência — não substitui a linha, a fonte ou a data que sustentam cada afirmação.
${runningFooter(report, { left: `${report.meta.subjectName ?? report.meta.municipalityName} · Leitura entre eras`, anchor: 'leitura-entre-eras', pageNo, })}
` /* ------------------------------------------------------------------ * * Trajetória completa (design cena 04) * * ------------------------------------------------------------------ */ const renderTrajectory = (report, pageNo) => `
${runningHead(report, { kicker: isSubjectUnit(report.unit) ? `${report.unit.docLabel} · ${report.meta.subjectName}` : `Dossiê cidade · ${report.meta.municipalityName}`, title: 'Trajetória completa', subtitle: 'Período, papel e como cada etapa foi recuperada', section: sectionLabel(report, 'trajetoria'), pageNo, })}
${tableHead(['Período', 'Papel', 'Como recuperar / lastro'])} ${report.trajectory .map( (row) => ``, ) .join('')}
Trajetória completa por período, papel e lastro
${htmlEscape(row.period)} ${htmlEscape(row.role)} ${htmlEscape(row.recovery)}${row.uncertain ? ` · ${htmlEscape(row.uncertain)}` : ''}

A trajetória contextualiza o período. Ela não prova vínculo com o recorte; esse vínculo só aparece quando a seção da era traz evidência do recorte.

${runningFooter(report, { left: `${report.meta.subjectName ?? report.meta.municipalityName} · Trajetória`, anchor: 'trajetoria-completa', pageNo, })}
` /* ------------------------------------------------------------------ * * Eras (design cena 05) — flowing sheets * * ------------------------------------------------------------------ */ 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 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
` /** Accepts the subject's `{ items }` capped objects and the city's plain arrays. */ const listOf = (value) => (Array.isArray(value) ? value : (value?.items ?? [])) /** Era units: value rows (table) then the "O que fez" list (action-list items). */ const eraUnits = (report, era, probe) => { 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 ?? '—')} ${phaseText(row.phase)} ${htmlEscape(unit.sphereLabels[row.sphere] ?? row.sphere)}${ row.sphere !== unit.defaultSphere ? ` · ${htmlEscape(unit.notSummedInline ?? 'não somar')}` : '' } ${sourceLink(row.sourceUrl, row.sourceDate ? formatDateBr(row.sourceDate) : SOURCE_LABEL)} `, }) } const actions = [ ...listOf(era.actions).map((action) => ({ title: action.title, brief: action.brief, detail: action.detail, sphere: action.sphere, scopeLabel: action.scopeLabel ?? null, sourceUrl: action.sourceUrl, })), // Units with their own honors sheet (institution) do not repeat them here. ...(report.unit.honorsSheet ? [] : listOf(era.honors).map((honor) => ({ title: honor.text, brief: honor.brief, detail: null, sphere: unit.defaultSphere, sourceUrl: honor.sourceUrl, }))), ] for (const action of actions) { units.push({ layout: 'action', html: ` ${copyHtml(briefOr(action.brief, action.title))} Alcance: ${htmlEscape(action.scopeLabel ?? unit.sphereLabels[action.sphere] ?? action.sphere ?? '—')}${ noteOr(action.brief, action.detail) ? ` · Lastro: ${copyHtml(noteOr(action.brief, action.detail))}` : ' · Lastro' } ${sourceLink(action.sourceUrl)} `, }) } return units } 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 `
${sectionHead('Objetos, valores e estágio', unit.eraNumbersTitle ?? 'Objeto, valor, fase e alcance')} ${eraNumbersTable(group.units, unit)}

“Não se aplica” diferencia atos sem valor financeiro. “Não localizado” não equivale a zero. Fase permanece texto, sem selo ou cor semântica.

` } return `
${sectionHead('O que fez · item — alcance — lastro', unit.eraActionsTitle ?? 'Papéis com evidência visível')}
    ${group.units.map((item) => item.html).join('')}
` }) .join('') return `
${runningHead(report, { kicker: era.label, title: `${era.subtitle} · ${era.period}`, subtitle: null, section: sectionLabel(report, `era-${era.id.toLowerCase()}`), pageNo, continuation: entry.index > 0 ? `continuação ${entry.index + 1}` : null, })} ${ entry.index === 0 && (era.narrative || era.summary) ? `

Leitura da era

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

` : '' } ${body} ${runningFooter(report, { left: `${report.meta.subjectName ?? report.meta.municipalityName} · ${era.label}`, anchor: `era-${era.id.toLowerCase()}`, pageNo, })}
` } const renderEraEmptySheet = (report, era, pageNo) => { const empty = report.unit.copy?.eraEmpty ?? {} return `
${runningHead(report, { kicker: era.label, title: `${era.subtitle} · ${era.period}`, subtitle: null, pageNo, })}

Lacuna explícita · não é “zero”

Nenhuma fonte suficiente foi localizada para esta era.

${htmlEscape(empty.body ?? 'A pesquisa não encontrou documento que sustente uma afirmação sobre o recorte.')}

O que foi feito

    ${(empty.work ?? []).map((item) => `
  • ${htmlEscape(item)}
  • `).join('')}

O que falta fazer

    ${(empty.todo ?? []).map((item) => `
  • ${htmlEscape(item)}
  • `).join('')}
Regra editorial: ${htmlEscape( typeof empty.rule === 'function' ? empty.rule(era.label) : 'Não preencher por memória, cargo provável ou texto de outra era.', )}
${runningFooter(report, { left: `${era.label} · lacuna explícita`, pageNo })}
` } const renderEraSheets = ({ report, era, pack, probe, nextPage }) => { if (era.omitted) return [] if (era.empty) return [renderEraEmptySheet(report, era, nextPage())] const units = eraUnits(report, era, probe) return packedSection({ key: `era:${era.id}`, anchor: `era-${era.id.toLowerCase()}`, units, pack, probe, }).map((entry) => renderEraSheet({ report, era, entry, probe, pageNo: nextPage() })) } /* ------------------------------------------------------------------ * * Região / polo (design cena 06) * * ------------------------------------------------------------------ */ const listYearRange = (items) => { const years = items .map((item) => item.year ?? (item.sourceDate ? String(item.sourceDate).slice(0, 4) : null)) .filter(Boolean) .sort() return years.length ? `${years[0]}–${years[years.length - 1]}` : null } /** Region recorte: callout + one labelled table sequence per list, packed by measurement. */ const regionUnits = (report, probe) => { const units = [] const lists = [ { list: report.region.municipal, label: 'Município · menção direta' }, { list: report.region.regional, label: 'Região / polo · alcance compartilhado' }, ] for (const { list, label } of lists) { const years = listYearRange(list.items) units.push({ layout: 'head', html: `${htmlEscape(label)} ${list.total} ${list.total === 1 ? 'item' : 'itens'} com fonte${years ? ` · ${htmlEscape(years)}` : ''}`, }) if (list.items.length === 0) { units.push({ layout: 'tr', html: `Nenhum item com fonte.`, }) continue } for (const item of list.items) { units.push({ layout: 'tr', html: ` ${copyHtml(briefOr(item.brief, item.answer))} ${htmlEscape(report.unit.sphereLabels[item.sphere] ?? item.sphere)} ${copyHtml(item.details ?? item.brief?.note ?? '—')} ${sourceLink(item.sourceUrl)} `, }) } } return units } const renderRegionSheet = ({ report, entry, probe, pageNo, isLast }) => `
${runningHead(report, { kicker: `Dossiê cidade · ${report.meta.municipalityName}`, title: 'Município, região e polo', subtitle: 'Alcances apresentados em sequências separadas', section: entry.index === 0 ? sectionLabel(report, 'regiao') : null, pageNo, continuation: entry.index > 0 ? `continuação ${entry.index + 1}` : null, })} ${ entry.index === 0 ? `
Não somar. ${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.

` : '' } ` : '' }
${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
${ isLast ? `

Cada linha informa sua esfera; ${htmlEscape(report.unit?.sumGuardNote ?? '')}

` : '' }
${runningFooter(report, { left: `${report.meta.municipalityName} · Região e polo`, anchor: 'regiao-e-polo', pageNo, })}
` const renderRegionSheets = ({ report, pack, probe, nextPage }) => { const units = regionUnits(report, probe) const entries = packedSection({ key: 'region', anchor: 'regiao', units, pack, probe }) return entries.map((entry, index) => renderRegionSheet({ report, entry, probe, pageNo: nextPage(), isLast: index === entries.length - 1, }), ) } /* ------------------------------------------------------------------ * * Abrangência / títulos (deltas instituição e tema, design cenas 09/10) * * ------------------------------------------------------------------ */ const subjectScopeColumns = (report) => report.unit.reachScene ? report.unit.reachScene.columns : ['Item', 'Abrangência', 'Evidência de alcance', 'Fonte'] const subjectScopeUnits = (report, probe) => { const columns = subjectScopeColumns(report) const units = [] for (const list of report.reach.lists) { const notSummable = list.sphere !== report.unit.defaultSphere const years = listYearRange(list.items) units.push({ layout: 'head', html: `${htmlEscape(list.label)} ${list.total} ${list.total === 1 ? 'item' : 'itens'} com fonte${notSummable ? ` · não somar ${htmlEscape(report.unit.notSummedTo)}` : ' · recorte direto'}${years ? ` · ${htmlEscape(years)}` : ''}`, }) if (list.items.length === 0) { units.push({ layout: 'tr', html: `Nenhum item com fonte.`, }) continue } for (const item of list.items) { if (!report.unit.reachScene) { units.push({ layout: 'tr', html: ` ${copyHtml(briefOr(item.brief, item.title))} ${htmlEscape(report.unit.sphereLabels[item.sphere] ?? item.sphere)} ${copyHtml(noteOr(item.brief, item.evidence))} ${sourceLink(item.sourceUrl)} `, }) continue } units.push({ layout: 'tr', html: ` ${copyHtml(briefOr(item.brief, item.title))} Era ${htmlEscape(item.era ?? '—')}${item.year ? ` · ${htmlEscape(item.year)}` : ''} ${valueCell(item.value)}${item.phase ? ` ${phaseText(item.phase)}` : ''} ${copyHtml(noteOr(item.brief, item.evidence))} ${sourceLink(item.sourceUrl)} `, }) } } return units } const renderSubjectScopeSheet = ({ report, entry, probe, pageNo, isLast }) => { const columns = subjectScopeColumns(report) const colgroup = report.unit.reachScene ? '' : '' return `
${runningHead(report, { kicker: `${report.unit.docLabel} · ${report.meta.subjectName}`, title: `Abrangência: ${report.unit.spheres.map((sphere) => report.unit.sphereLabels[sphere]).join(' × ')}`, subtitle: 'Três recortes, três leituras, nenhuma soma combinada', section: entry.index === 0 ? sectionLabel(report, 'abrangencia') : null, pageNo, continuation: entry.index > 0 ? `continuação ${entry.index + 1}` : null, })} ${ entry.index === 0 ? `
Não somar. ${htmlEscape(report.reach.ruleBody)}
` : '' }
${colgroup} ${tableHead(columns)} ${entry.chunk.map((unit) => unit.html).join('')}
Itens do recorte com evidência de alcance e fonte
${ isLast ? `

Cada linha informa sua abrangência; ${htmlEscape(report.unit.sumGuardNote ?? '')}

` : '' }
${runningFooter(report, { left: `${report.meta.subjectName} · Abrangência`, anchor: 'abrangencia', pageNo, })}
` } const renderSubjectScopeSheets = ({ report, pack, probe, nextPage }) => { const units = subjectScopeUnits(report, probe) const entries = packedSection({ key: 'scope', anchor: 'abrangencia', units, pack, probe }) return entries.map((entry, index) => renderSubjectScopeSheet({ report, entry, probe, pageNo: nextPage(), isLast: index === entries.length - 1, }), ) } const subjectHonors = (report) => report.eras.find((era) => era.id === 'C')?.honors?.items ?? [] const subjectHonorUnits = (report, probe) => subjectHonors(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 honorDate = (honor) => { if (honor.date) return formatDateBr(honor.date) if (honor.year) return String(honor.year) return '—' } const renderSubjectHonorSheet = ({ report, entry, probe, pageNo }) => `
${runningHead(report, { kicker: `${report.unit.docLabel} · ${report.meta.subjectName}`, title: 'Títulos, honrarias e vínculos', subtitle: 'Reconhecimento não substitui entrega; vínculo não prova resultado', pageNo, continuation: entry.index > 0 ? `continuação ${entry.index + 1}` : null, })}
${tableHead(['Data', 'Natureza', 'Registro', 'Fonte'])} ${entry.chunk.map((unit) => unit.html).join('')}
Títulos, honrarias e vínculos por data e fonte
Como ler: vínculo documentado informa papel, unidade e período — não autoriza atribuir toda ação posterior do recorte. Honraria registra reconhecimento e órgão concedente — não vira prova de recurso ou entrega.
${runningFooter(report, { left: 'Títulos e vínculos', anchor: 'titulos', pageNo })}
` const renderSubjectHonorSheets = ({ report, pack, probe, nextPage }) => { const units = subjectHonorUnits(report, probe) if (units.length === 0) return [] return packedSection({ key: 'honors', anchor: 'titulos', units, pack, probe }).map((entry) => renderSubjectHonorSheet({ report, entry, probe, pageNo: nextPage() }), ) } /* ------------------------------------------------------------------ * * O que Solla defende (design cena 11) * * ------------------------------------------------------------------ */ const renderDefendSheet = ({ report, entry, probe, pageNo }) => { const { positions, gaps } = report.defends return `
${runningHead(report, { kicker: isSubjectUnit(report.unit) ? `${report.unit.docLabel} · ${report.meta.subjectName}` : `Dossiê cidade · ${report.meta.municipalityName}`, title: 'O que Solla defende', subtitle: 'Posições e prioridades com lastro identificado', section: entry.index === 0 ? sectionLabel(report, 'defende') : null, pageNo, continuation: entry.index > 0 ? `continuação ${entry.index + 1}` : null, })} ${ entry.index === 0 ? `

Leitura dos registros · não é opinião

${ positions.length ? `As posições abaixo vêm de registros com fonte do recorte (ato, notícia datada ou trecho identificado do acervo).${ gaps.length ? ` ${gaps.length} ${gaps.length === 1 ? 'frente aparece' : 'frentes aparecem'} como lacuna explícita.` : '' }` : 'Nenhuma posição com lastro foi localizada no recorte — o que falta vira lacuna explícita, nunca inferência.' }

` : '' }

Posições e defesas localizadas

    ${entry.chunk.map((unit) => unit.html).join('')}
${ entry.index === 0 ? `
Como usar: “Defende” só entra quando há ato, notícia datada ou fala identificada como lastro. Apoio ou articulação não vira promessa. Sem registro, a linha não existe ou aparece como lacuna explícita.
` : '' } ${runningFooter(report, { left: `${report.meta.subjectName ?? report.meta.municipalityName} · O que Solla defende`, anchor: 'o-que-solla-defende', pageNo, })}
` } const defendUnits = (report, probe) => { const units = [] for (const position of report.defends.positions) { units.push({ layout: 'li', html: ` ${htmlEscape(position.label)}

${copyHtml(briefOr(position.brief, position.reading))}${ noteOr(position.brief, position.details) ? ` ${copyHtml(noteOr(position.brief, position.details))}` : '' }

${ position.sourceDate ? `${formatDateBr(position.sourceDate)} · ` : '' }Era ${htmlEscape(position.era)} ${sourceLink(position.sourceUrl)}

`, }) } for (const gap of report.defends.gaps) { units.push({ layout: 'li', html: ` ${htmlEscape(gap.label)}

Sem registro localizado. ${htmlEscape(gap.reason)}

Lacuna explícita${gap.era ? ` · Era ${htmlEscape(gap.era)}` : ''}

`, }) } return units } const renderDefendSheets = ({ report, pack, probe, nextPage }) => { const units = defendUnits(report, probe) if (units.length === 0) { return [ renderDefendSheet({ report, entry: { anchor: 'defende', chunk: [], index: 0 }, probe: false, pageNo: nextPage(), }), ] } return packedSection({ key: 'defends', anchor: 'defende', units, pack, probe }).map((entry) => renderDefendSheet({ report, entry, probe, pageNo: nextPage() }), ) } /* ------------------------------------------------------------------ * * Lacunas, notícias, acervo, fontes (design cenas 07/08) * * ------------------------------------------------------------------ */ const gapUnits = (report, probe) => report.gaps.map((gap, index) => { const attrs = unitAttrs(probe, index, 'tr') if (!report.unit.gapsScene) { return { layout: 'tr', html: ` ${htmlEscape(gap.label)} ${htmlEscape(gap.reason)} Não completar por inferência. ${htmlEscape(gap.nextStep)} `, } } const era = gap.era && gap.era !== 'Acervo' ? `Era ${gap.era}` : 'Acervo' return { layout: 'tr', html: ` ${htmlEscape(era)} ${htmlEscape(gap.label)} ${htmlEscape(gap.reason)} ${htmlEscape(gap.nextStep)} ${index === 0 ? 'prioridade' : 'aberta'} `, } }) const renderGapSheet = ({ report, entry, probe, pageNo, isLast = false }) => { const scene = report.unit.gapsScene const columns = scene ? scene.columns : ['Lacuna', 'Situação da busca', 'Limite editorial', 'Próximo passo'] const colgroup = scene ? '' : '' return `
${runningHead(report, { kicker: isSubjectUnit(report.unit) ? `${report.unit.docLabel} · ${report.meta.subjectName}` : `Dossiê cidade · ${report.meta.municipalityName}`, title: 'Lacunas explícitas', subtitle: 'O que não foi encontrado e qual é o próximo passo', pageNo, continuation: entry.index > 0 ? `continuação ${entry.index + 1}` : null, })}
${colgroup} ${tableHead(columns)} ${ 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.

Ausência é resultado: resultado de busca não prova ausência histórica.

${scene && isLast ? `

${htmlEscape(scene.alert)}

` : ''}
${runningFooter(report, { left: 'Lacunas explícitas', anchor: 'lacunas-explicitas', 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(), isLast: true, }), ] } const entries = packedSection({ key: 'gaps', anchor: 'lacunas', units, pack, probe }) return entries.map((entry, index) => renderGapSheet({ report, entry, probe, pageNo: nextPage(), isLast: index === entries.length - 1, }), ) } const newsUnits = (report, probe) => report.news.map((row, index) => ({ layout: 'tr', html: ` ${htmlEscape(formatDateBr(row.date))} ${htmlEscape(row.outlet)} ${htmlEscape(row.title)}${ row.summary ? `
${htmlEscape(row.summary)}` : '' } ${row.era ? `Era ${htmlEscape(row.era)}` : '—'} `, })) const renderNewsSheet = ({ report, entry, probe, pageNo }) => `
${runningHead(report, { kicker: isSubjectUnit(report.unit) ? `${report.unit.docLabel} · ${report.meta.subjectName}` : `Dossiê cidade · ${report.meta.municipalityName}`, title: 'Notícias, acervo e fontes', subtitle: 'Rastro de consulta e limites de cobertura', pageNo, continuation: entry.index > 0 ? `continuação ${entry.index + 1}` : null, })}

Notícias e documentos consultados · amostra

${tableHead(['Data', 'Origem', 'Documento', 'Uso no dossiê'])} ${entry.chunk.map((unit) => unit.html).join('')}
Fontes consultadas: data, origem, documento e uso no dossiê
${runningFooter(report, { left: 'Notícias e documentos', anchor: 'noticias', 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) => { const attrs = unitAttrs(probe, index, 'tr') if (!report.unit.acervoScene) { return { layout: 'li', html: `${htmlEscape(row.period)} — ${copyHtml(row.text)} ${sourceLink(row.sourceUrl)}`, } } const identity = report.meta.identity ?? {} return { layout: 'tr', html: ` ${htmlEscape(row.period)} ${htmlEscape(identity.label ?? '—')} ${copyHtml(row.text)}${row.excerpt ? `
${copyHtml(row.excerpt)}` : ''} ${htmlEscape(report.unit.sphereLabels[report.unit.defaultSphere])} ${sourceLink(row.sourceUrl)} `, } }) const renderAcervoSheet = ({ report, entry, probe, pageNo, isLast }) => { const scene = report.unit.acervoScene return `
${runningHead(report, { kicker: isSubjectUnit(report.unit) ? `${report.unit.docLabel} · ${report.meta.subjectName}` : `Dossiê cidade · ${report.meta.municipalityName}`, title: 'Acervo interno de falas (amostra)', subtitle: `Base Teqo 2011+ · ${report.acervo.items.length} de ${report.acervo.recorte} falas do recorte, com link`, pageNo, continuation: entry.index > 0 ? `continuação ${entry.index + 1}` : null, })}

Falas do mandato sobre o recorte

${ scene ? ` ${tableHead(scene.columns)} ${entry.chunk.map((unit) => unit.html).join('')}
Amostra do acervo por data, tema canônico, trecho e abrangência
` : `
    ${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 ?? '')}

` : '' } ${scene && isLast ? `

${htmlEscape(scene.alert)}

` : ''}
${runningFooter(report, { left: 'Acervo interno · amostra', anchor: 'acervo', 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 renderSources = (report, pageNo) => `
${runningHead(report, { kicker: isSubjectUnit(report.unit) ? `${report.unit.docLabel} · ${report.meta.subjectName}` : `Dossiê cidade · ${report.meta.municipalityName}`, title: 'Fontes e limites', subtitle: 'O que foi consultado e o que não se pode afirmar', pageNo, })} ${ report.unit.sourcesHierarchy ? `

Hierarquia de fontes

${tableHead(['Camada', 'Uso', 'Limite'])} ${report.unit.sourcesHierarchy .map( (row) => ``, ) .join('')}
Camadas de fonte, seu uso e seu limite
${htmlEscape(row.layer)}${htmlEscape(row.use)}${htmlEscape(row.limit)}
` : '' } ${ report.acervo?.recorte ? `
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. ${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('')}
Nota de defeso eleitoral 2026 · insumo interno não é autorização para publicar. Publicidade institucional e propaganda eleitoral obedecem a regimes diferentes. Antes de adaptar qualquer trecho para peça externa, submeta texto, imagem, autoria, período e canal à revisão responsável. Este dossiê organiza evidências; não substitui análise jurídica nem editorial.
${runningFooter(report, { left: 'Fontes e limites', anchor: 'fontes-e-limites', pageNo, })}
` /* ------------------------------------------------------------------ * * Print CSS (port of the approved hi-fi tokens and classes) * * ------------------------------------------------------------------ */ const DOSSIER_PRINT_CSS = ` * { box-sizing: border-box; } html, body { margin: 0; padding: 0; } body { font-family: 'Fira Sans', 'DejaVu Sans', Arial, sans-serif; color: var(--ink); background: #fff; } :root { --ink: #18232d; --ink-soft: #52616d; --accent: #4d6d7c; --line: #bdc8ce; --line-light: #dce3e6; --paper: #ffffff; --note: #f2f5f6; } @page { size: A4; margin: 0; } .sheet { position: relative; display: flex; width: 210mm; height: 297mm; min-height: 297mm; padding: 15mm 17mm 13mm; overflow: hidden; flex-direction: column; background: var(--paper); font-size: 9.4pt; line-height: 1.42; break-after: page; } .sheet:last-child { break-after: auto; } .sheet p, .sheet ul, .sheet ol, .sheet dl, .sheet h1, .sheet h2, .sheet h3 { margin-top: 0; } .sheet a { color: #315d70; text-decoration: underline; text-underline-offset: 1.5px; } .m-0 { margin: 0; } .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); } .running-head { display: flex; align-items: flex-start; justify-content: space-between; gap: 10mm; padding-bottom: 3mm; border-bottom: .35mm solid var(--ink); } .running-kicker { margin: 0; color: var(--accent); font-size: 7.5pt; font-weight: 700; letter-spacing: .11em; text-transform: uppercase; } .running-title { margin: .8mm 0 0; font-size: 17pt; font-weight: 650; line-height: 1.08; letter-spacing: -.025em; } .running-subtitle { margin: 1mm 0 0; color: var(--ink-soft); font-size: 8.7pt; } .running-meta { min-width: 41mm; margin: 0; color: var(--ink-soft); font-size: 7.3pt; line-height: 1.45; text-align: right; font-variant-numeric: tabular-nums; } .running-meta p { margin: 0; } .ink-strong { color: var(--ink); } .running-footer { display: flex; margin-top: auto; padding-top: 2.6mm; border-top: .25mm solid var(--line); align-items: flex-end; justify-content: space-between; gap: 8mm; color: var(--ink-soft); font-size: 7.2pt; line-height: 1.3; } .folio { color: var(--ink); font-weight: 700; font-variant-numeric: tabular-nums; white-space: nowrap; } .section { margin-top: 5mm; } .section + .section { margin-top: 6mm; } .section-head { margin-bottom: 1mm; } .eyebrow { margin: 0 0 1.3mm; color: var(--accent); font-size: 7.2pt; font-weight: 700; letter-spacing: .1em; text-transform: uppercase; } .section-title { margin: 0; font-size: 13pt; font-weight: 650; line-height: 1.18; letter-spacing: -.012em; } .lede { max-width: 155mm; margin: 3mm 0 0; font-size: 10.6pt; line-height: 1.52; } .body-copy { margin: 2.6mm 0 0; font-size: 9.6pt; line-height: 1.5; } .muted { color: var(--ink-soft); } .small { font-size: 8.2pt; } .source-link { color: #315d70; text-decoration: underline; text-underline-offset: 1.5px; } .phase-text { font-variant-numeric: tabular-nums; } .plain-list { margin: 3mm 0 0; padding: 0; list-style: none; } .plain-list li { position: relative; padding: 2.2mm 0 2.2mm 6mm; border-top: .2mm solid var(--line-light); } .plain-list li:last-child { border-bottom: .2mm solid var(--line-light); } .plain-list li::before { position: absolute; top: 3.3mm; left: 0; width: 2.2mm; height: .45mm; background: var(--accent); content: ''; } .fact-list strong, .analysis-list strong { font-weight: 700; } .analysis-list li { padding-top: 2.8mm; padding-bottom: 2.8mm; } .index-list { margin: 3mm 0 0; padding: 0; list-style: none; } .index-list li { display: flex; padding: 1.25mm 0; border-bottom: .2mm dotted var(--line); align-items: baseline; gap: 2mm; } .index-list .index-number { width: 8mm; color: var(--accent); font-size: 7.5pt; font-weight: 700; font-variant-numeric: tabular-nums; } .index-list .index-label { font-weight: 600; } .index-list .index-page { margin-left: auto; } .document-table { width: 100%; margin-top: 3mm; border-collapse: collapse; table-layout: fixed; font-size: 8.2pt; line-height: 1.34; } .document-table th { padding: 1.5mm 1.3mm 1.4mm; border-top: .35mm solid var(--ink); border-bottom: .25mm solid var(--ink); color: var(--ink-soft); font-size: 6.8pt; font-weight: 700; letter-spacing: .045em; text-align: left; text-transform: uppercase; vertical-align: bottom; } .document-table td { padding: 1.8mm 1.3mm; border-bottom: .2mm solid var(--line-light); vertical-align: top; overflow-wrap: anywhere; } .document-table .tabular { font-variant-numeric: tabular-nums; } .document-table .strong-row td:first-child { font-weight: 700; } .document-table .list-head td { padding-top: 4mm; border-bottom: .35mm solid var(--ink); } .document-table .list-head .eyebrow { margin: 0; } .table-note { margin: 1.7mm 0 0; color: var(--ink-soft); font-size: 7.6pt; } .callout-line { margin-top: 4mm; padding: 2.4mm 0 2.4mm 4mm; border-top: .25mm solid var(--line); border-bottom: .25mm solid var(--line); border-left: 1mm solid var(--accent); color: var(--ink-soft); font-size: 8.4pt; line-height: 1.42; } .note-line { margin-top: 3mm; padding: 2mm 0; border-top: .25mm solid var(--line); border-bottom: .25mm solid var(--line); color: var(--ink-soft); font-size: 8.2pt; } .note-line strong { color: var(--ink); } .cover-series { display: flex; align-items: flex-start; justify-content: space-between; gap: 12mm; padding-bottom: 5mm; border-bottom: .3mm solid var(--ink); } .cover-series p { margin: 0; } .cover-series-name { font-size: 8pt; font-weight: 700; letter-spacing: .12em; text-transform: uppercase; } .cover-internal { color: var(--ink-soft); font-size: 7.5pt; line-height: 1.45; text-align: right; } .cover-main { margin-top: 38mm; } .cover-overline { margin: 0 0 4mm; color: var(--accent); font-size: 8pt; font-weight: 700; letter-spacing: .12em; text-transform: uppercase; } .cover-title { max-width: 160mm; margin: 0; font-size: 35pt; font-weight: 650; line-height: .98; letter-spacing: -.04em; } .cover-title span { color: var(--accent); } .cover-subtitle { max-width: 136mm; margin: 6mm 0 0; color: var(--ink-soft); font-size: 13pt; line-height: 1.4; } .cover-data { display: grid; margin: 24mm 0 0; border-top: .3mm solid var(--ink); grid-template-columns: 45mm 1fr; } .cover-data dt, .cover-data dd { margin: 0; padding: 2.7mm 0; border-bottom: .2mm solid var(--line); } .cover-data dt { color: var(--ink-soft); font-size: 7.5pt; font-weight: 700; letter-spacing: .035em; text-transform: uppercase; } .cover-data dd { font-size: 9pt; font-weight: 600; } .cover-how { max-width: 145mm; margin-top: 9mm; padding-top: 3mm; border-top: .6mm solid var(--accent); } .cover-how p { margin: 0; font-size: 9pt; line-height: 1.45; color: var(--ink-soft); } .cover-how strong { display: block; margin-bottom: 1.5mm; font-size: 8pt; letter-spacing: .07em; text-transform: uppercase; } .cover-footer { display: flex; margin-top: auto; padding-top: 4mm; border-top: .3mm solid var(--ink); align-items: flex-end; justify-content: space-between; gap: 8mm; color: var(--ink-soft); font-size: 7.6pt; } .cover-stamp { margin: 0; text-align: right; font-weight: 700; text-transform: uppercase; letter-spacing: .08em; color: var(--accent); } .trajectory-table td:first-child { color: var(--accent); font-weight: 700; font-variant-numeric: tabular-nums; white-space: nowrap; } .trajectory-table td:nth-child(2) { font-weight: 650; } .era-reading { margin-top: 4mm; padding-left: 4mm; border-left: .9mm solid var(--accent); } .era-reading p:last-child { margin-bottom: 0; } .action-list { margin: 3mm 0 0; padding: 0; list-style: none; } .action-list li { padding: 2.2mm 0; border-top: .2mm solid var(--line-light); } .action-list li:last-child { border-bottom: .2mm solid var(--line-light); } .action-list .action-title { display: block; font-weight: 700; } .action-list .action-meta { display: block; margin-top: .8mm; color: var(--ink-soft); font-size: 7.8pt; } .continuation { color: var(--accent); font-size: 7.3pt; font-weight: 700; letter-spacing: .04em; text-transform: uppercase; } .placeholder-bar { display: inline-block; width: 28mm; height: 2.3mm; border-radius: .6mm; background: #d7dee2; vertical-align: middle; } .position-list { margin: 3mm 0 0; padding: 0; list-style: none; } .position-list li { display: grid; padding: 3mm 0; border-top: .2mm solid var(--line-light); grid-template-columns: 39mm 1fr 42mm; gap: 4mm; align-items: start; } .position-list li:last-child { border-bottom: .2mm solid var(--line-light); } .position-label { color: var(--accent); font-size: 7.2pt; font-weight: 700; letter-spacing: .05em; text-transform: uppercase; } .position-reading { margin: 0; font-size: 9pt; line-height: 1.44; } .position-source { margin: 0; color: var(--ink-soft); font-size: 7.4pt; line-height: 1.38; } .empty-panel { border: .45mm dashed var(--accent); color: var(--ink-soft); padding: 5mm 8mm; text-align: center; } .empty-panel .rule-title { margin: 1.5mm 0; font-size: 12pt; font-weight: 700; color: var(--ink); } .defeso { margin-top: auto; } ` /** * Probe-only CSS: the packed sections measure their true content height, with * no page clamp and no top margins (the packer adds the row gap back). */ const DOSSIER_PROBE_CSS = ` .sheet { height: auto; min-height: 0; overflow: visible; } [data-pack-section] .section { margin-top: 0; } [data-pack-section] .document-table, [data-pack-section] .action-list, [data-pack-section] .plain-list, [data-pack-section] .position-list { margin-top: 0; } [data-pack-section] .table-note, [data-pack-section] .callout-line { margin-top: 0; } ` /* ------------------------------------------------------------------ * * Anchors of the packed sections (the builder inverts the map) * * ------------------------------------------------------------------ */ const SHARED_PACK_ANCHORS = { 'era-a': 'era:A', 'era-b': 'era:B', 'era-c': 'era:C', lacunas: 'gaps', noticias: 'news', acervo: 'acervo', defende: 'defends', } const MUNICIPALITY_PACK_ANCHORS = { ...SHARED_PACK_ANCHORS, regiao: 'region', } export const INSTITUTION_PACK_ANCHORS = { ...SHARED_PACK_ANCHORS, titulos: 'honors', abrangencia: 'scope', } const THEME_PACK_ANCHORS = { ...SHARED_PACK_ANCHORS, abrangencia: 'scope', } const PACK_ANCHORS_BY_UNIT = { municipality: MUNICIPALITY_PACK_ANCHORS, institution: INSTITUTION_PACK_ANCHORS, theme: THEME_PACK_ANCHORS, } /** Anchor map of the unit's packed sections (the builder inverts it). */ export const dossierPackAnchors = (unit) => PACK_ANCHORS_BY_UNIT[resolveDossierUnit(unit).id] ?? MUNICIPALITY_PACK_ANCHORS /* ------------------------------------------------------------------ * * Document assembly (two passes: the index needs the page numbers) * * ------------------------------------------------------------------ */ const buildDossierParts = (report, { pack, probe, pages, indexMode }) => { let page = 0 const nextPage = () => ++page const parts = [] parts.push(renderCover(report)) page += 1 parts.push(renderEssentials(report, { pageNo: nextPage(), pages, indexMode })) parts.push(renderBetweenEras(report, nextPage())) parts.push(renderTrajectory(report, nextPage())) for (const era of report.eras) { parts.push(...renderEraSheets({ report, era, pack, probe, nextPage })) } if (isSubjectUnit(report.unit)) { parts.push(...renderSubjectScopeSheets({ report, pack, probe, nextPage })) if (report.unit.honorsSheet) { parts.push(...renderSubjectHonorSheets({ report, pack, probe, nextPage })) } } else { parts.push(...renderRegionSheets({ report, pack, probe, nextPage })) } parts.push(...renderDefendSheets({ 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(renderSources(report, nextPage())) return parts } const renderDossierDocument = ( report, { pack = null, probe = false, indexMode = 'pages' } = {}, ) => { const first = buildDossierParts(report, { pack, probe, pages: null, indexMode: 'labels' }) const pages = {} first.forEach((html, index) => { const match = /data-page="([^"]+)"/.exec(html) if (match) pages[match[1]] = index + 1 }) report.meta.pageTotal = first.length // Probe and label modes have no page numbers to print: the first pass is final. const parts = probe || indexMode === 'labels' ? first : buildDossierParts(report, { pack, probe, pages, indexMode }) const title = isSubjectUnit(report.unit) ? `${report.meta.title} — ${report.meta.subjectName}` : `${report.meta.title} — ${report.meta.municipalityName}` return ` ${htmlEscape(title)}${probe ? `` : ''} ${parts.join('\n')} ` } export const renderDossierHtml = (report, options) => renderDossierDocument(report, options) /* ------------------------------------------------------------------ * * Companion .md (mirrors the sections, keeps the sources clickable) * * ------------------------------------------------------------------ */ const mdIndex = (report) => { const lines = ['## Índice', ''] for (const entry of buildIndexEntries(report)) { lines.push( entry.included ? `- [${entry.heading}](#${entry.mdAnchor})` : `${entry.heading} — sem evidência suficiente; consulte as lacunas`, ) } return lines } const mdEra = (era) => { const lines = [ `## ${era.label} (${era.period})`, '', `_${era.method}_`, '', `Trilha: ${era.recovery}`, ] if (era.omitted) { lines.push('', '> Lacuna explícita: nenhum ponto com fonte foi localizado para esta era.') return lines.join('\n') } 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') } /** Head of the companion: O essencial, Leitura entre eras, Trajetória e eras. */ const mdHead = (report) => { const lines = ['', '', '## O essencial', ''] 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._`, '', '### Fatos-chave', '', ) for (const fact of report.essentials.facts) lines.push(`- **${fact.label}** ${fact.text}`) lines.push('', '', '', '## Leitura entre eras', '') for (const bullet of report.betweenEras.bullets) { lines.push(`- ${bullet.label ? `**${bullet.label}** ` : ''}${bullet.text}`) } lines.push( '', '### Síntese operacional', '', '| Era | Onde está a evidência | Como citar com segurança | Limite |', '| --- | --- | --- | --- |', ) for (const row of report.betweenEras.table) { lines.push(`| ${row.era} | ${row.where} | ${row.howToCite} | ${row.limit} |`) } lines.push('', '', '', '## Trajetória completa', '') for (const row of report.trajectory) { lines.push( `- **${row.period}** — ${row.role}${row.uncertain ? ` _(${row.uncertain})_` : ''} · ${row.recovery}`, ) } for (const era of report.eras) { lines.push('', ``, '', mdEra(era)) } return lines } /** Unit section of the companion: Região/polo (city) or Abrangência + títulos. */ const mdUnitSection = (report) => { const lines = [] if (isSubjectUnit(report.unit)) { lines.push('', '') lines.push( `## Abrangência: ${report.unit.spheres.map((sphere) => report.unit.sphereLabels[sphere]).join(' × ')}`, '', `> ${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})`, ) } lines.push('') } const honors = report.eras.find((era) => era.id === 'C')?.honors?.items ?? [] if (report.unit.honorsSheet && honors.length) { lines.push('', '', '## Títulos, honrarias e vínculos', '') for (const honor of honors) { lines.push( `- **${stripInlineSources(briefOr(honor.brief, honor.text))}**${noteOr(honor.brief, honor.detail) ? ` — ${stripInlineSources(noteOr(honor.brief, honor.detail))}` : ''}${honor.date ? ` · ${formatDateBr(honor.date)}` : honor.year ? ` · ${honor.year}` : ''} [fonte](${honor.sourceUrl})`, ) } } return lines } lines.push('', '', '## Região / polo', '') lines.push(`> ${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})`, ) } return lines } /** Tail of the companion: defende, lacunas, notícias, acervo e limites. */ const mdTail = (report) => { const lines = ['', '', '## O que Solla defende', ''] if (report.defends.positions.length === 0) lines.push('_Nenhuma posição com lastro localizada._') for (const position of report.defends.positions) { lines.push( '', `- **${position.label}:** ${stripInlineSources(briefOr(position.brief, position.reading))}${noteOr(position.brief, position.details) ? ` — ${stripInlineSources(noteOr(position.brief, position.details))}` : ''} [fonte](${position.sourceUrl})`, ) } for (const gap of report.defends.gaps) { lines.push( '', `- **${gap.label}:** sem registro localizado — ${gap.reason}${gap.era ? ` (Era ${gap.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.era ? ` · Era ${row.era}` : ''} — ${row.title} — ${row.url}${row.summary ? ` — ${row.summary}` : ''}`, ) } } 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( '', '', '', '## 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 } const renderMunicipalityDossierMd = (report) => `${[ `# ${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.', '', ...mdIndex(report), '', ...mdHead(report), ...mdUnitSection(report), ...mdTail(report), ].join('\n')}\n` const renderSubjectDossierMd = (report) => `${[ `# ${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', '', ...report.unit .identityRows(report.meta.identity ?? {}) .map(([key, value]) => `- **${key}:** ${value}`), '', ...mdIndex(report), '', ...mdHead(report), ...mdUnitSection(report), ...mdTail(report), ].join('\n')}\n` export const renderDossierMd = (report) => isSubjectUnit(report.unit) ? renderSubjectDossierMd(report) : renderMunicipalityDossierMd(report)