mirror of
https://github.com/fsolla/teqo.git
synced 2026-11-13 06:43:06 +00:00
fix(C190): boletim de uma página cabe por medição (painel de acervo + fit)
O run real (Educação) entregava o dossiê mas o boletim media 3502px vs 1133px úteis: os 15 fatos do acervo (sphere area) dominavam os destaques com o summary integral e não havia contador nem fit no one-pager. - dossieBlocks marca as falas do acervo com sourcePanel: true; buildBulletin as mantém contadas no "e mais N" e fora das vagas impressas (destaques/E mais ficam com findings com fonte) - buildBulletin ganha printLimit; emitHtmlPairPdf ganha onBulletinOverflow (espelho do onDossierOverflow do C188) e o builder do tema re-renderiza com 2 fatos a menos até caber, fail-closed - boletim subject (tema/instituição) ganha o contador C188 que só o município tinha, com copy.remaining por descritor - contrato do tema documenta brief/summary (C188) na skill e no subagente researcher
This commit is contained in:
parent
e5e1ae7ee8
commit
9207553863
10 changed files with 188 additions and 44 deletions
|
|
@ -194,7 +194,12 @@ consolidação determinística.
|
|||
{
|
||||
"id": "era_c_emendas", // id do checklist da era
|
||||
"answer": "R$ 5,0 milhões para a rede de ensino",
|
||||
"summary": "opcional — versão curta, completa e auto-contida do answer, redigida para caber no resumo/boletim (C188)",
|
||||
"details": "opcional; aceita {{fonte}} / {{fonte:N}}",
|
||||
"brief": { // obrigatório no item publicado: copy reformulada, curta
|
||||
"title": "≤80 chars — manchete (o quê + onde)",
|
||||
"note": "≤120 chars — 1 frase de contexto; opcional"
|
||||
},
|
||||
"sphere": "area", // area | segmento | rede (default area)
|
||||
"numbers": [ // opcional
|
||||
{ "label": "Emenda", "value": "R$ 5,0 mi", "year": "2024", "phase": "empenhado" }
|
||||
|
|
@ -229,6 +234,17 @@ vira lacuna. `phase` ∈ `autorizado|empenhado|liquidado|pago|restos` (default
|
|||
`nao_informado`; empenho **não** é pagamento). O `bulletinFacts` (ledger do
|
||||
boletim) só é populado por item **com fonte** — o boletim não introduz fato novo.
|
||||
|
||||
**`brief` (copy reformulada, sem reticências) e `summary` (C188).** As páginas
|
||||
A4 têm altura fixa: o `brief` é a copy que o PDF imprime nas páginas de era, nas
|
||||
listas de abrangência e nos cartões do boletim (o `answer`/`details` integrais
|
||||
ficam no `.md` e no lastro); sem ele o builder imprime o texto integral e a
|
||||
página pode estourar. Escreva `brief.title` ≤ 80 e `brief.note` ≤ 120 chars para
|
||||
**todo item publicado**; o `summary` alimenta as superfícies de resumo. O
|
||||
boletim de uma página **mede e reduz por medição**: o painel do acervo (falas
|
||||
com link) entra no contador "e mais N fatos com fonte", nunca nos destaques; se
|
||||
ainda não couber, o builder re-renderiza com menos fatos impressos e declara o
|
||||
resto — sem truncar texto e sem descarte silencioso.
|
||||
|
||||
`<slug>.narrative.json` (opcional; escrito pelo redator, auditado pelo
|
||||
orquestrador):
|
||||
|
||||
|
|
@ -299,7 +315,10 @@ nenhum número, data, nome ou órgão novo.
|
|||
abrangência", número em destaque, título curto, nota); timeline de 4 passos da
|
||||
trajetória; **"E mais" com ≤14 itens** em duas colunas; rodapé com controle
|
||||
editorial e defeso. Com **poucos fatos**, usa a variação de lacuna (não cria
|
||||
cards vazios para chegar a seis).
|
||||
cards vazios para chegar a seis). O painel do acervo **não ocupa vaga de
|
||||
destaque**: entra no contador "e mais N fatos com fonte"; se os fatos
|
||||
impressos ainda estourarem a folha, o builder mede e reduz o conjunto
|
||||
impresso até caber, declarando o resto — nunca corta texto.
|
||||
- **Sem declaração de fontes** (as fontes vivem exclusivamente no dossiê), sem
|
||||
CTA de campanha; herda **apenas** fatos com fonte do dossiê.
|
||||
|
||||
|
|
|
|||
|
|
@ -18,7 +18,10 @@ slug da área na taxonomia e coordena as demais eras/áreas em paralelo.
|
|||
2. Fazer a **pesquisa web datada** da era e escrever
|
||||
`data/dossie-solla-tema/<slug>.<era>.research.json` no contrato da skill
|
||||
(ex.: `educacao.c.research.json`) — item sem `sourceUrl`/`sourceDate` não vai
|
||||
para o arquivo (vira lacuna explícita). Use o checklist da era e marque
|
||||
para o arquivo (vira lacuna explícita). Todo item publicado leva `brief`
|
||||
(`title` ≤ 80, `note` ≤ 120 chars) e, quando ajudar o resumo, `summary`
|
||||
curto e auto-contido (C188): é a copy que o PDF e o boletim imprimem; sem ela
|
||||
o texto integral pode estourar a página. Use o checklist da era e marque
|
||||
`sphere` (`area`/`segmento`/`rede`); segmento e rede **nunca** são somados à
|
||||
área.
|
||||
3. Devolver **apenas o recibo curto** (seção "Recibo do researcher" da skill):
|
||||
|
|
|
|||
1
docs/changelog/2026-09-18-c190-boletim-fit.md
Normal file
1
docs/changelog/2026-09-18-c190-boletim-fit.md
Normal file
|
|
@ -0,0 +1 @@
|
|||
**C190-fit — boletim de uma página cabe por medição (2026-09-18):** o primeiro run real do `/dossie-solla-tema` (Educação) entregou o dossiê mas **não** o boletim: com o acervo real (15 falas) o one-pager media 3502px contra 1133px úteis. Três correções no dono, sem geminar pipeline: (1) **acervo é painel de fontes, não achado** — os fatos de fala entram no `bulletinFacts` marcados `sourcePanel: true` (`dossieBlocks`) e o `buildBulletin` os mantém **contados** no "e mais N fatos com fonte" mas **fora das vagas impressas** (destaques e "E mais"), que passam a ser só findings com fonte; (2) **fit por medição** — `emitHtmlPairPdf` ganha `onBulletinOverflow` (espelho do `onDossierOverflow` do C188), e o builder do tema re-renderiza com `printLimit` dois fatos menor até a folha caber, sempre declarando o resto (fail-closed se o callback não encolher); (3) **contador C188 no boletim `subject`** — tema/instituição não exibiam o "e mais N" que o C188 exige (o renderer de município já tinha); o rodapé ganha `copy.remaining` por descritor ("no dossiê da área/da instituição"). O contrato do tema (skill + subagente) passa a documentar `brief` (title ≤80 / note ≤120, obrigatório no item publicado) e `summary` (C188) — a copy impressa que faltava deixava o `answer`/`details` integrais irem para as páginas. Specs novos em `dossieTheme.unit.spec.ts` (painel contado fora dos destaques; `printLimit` encolhe e declara o resto); specs existentes de município/instituição seguem verdes (testes 3772/3772). Sem schema, migration, URL pública, Consent ou LGPD — scripts/skill/testes apenas.
|
||||
|
|
@ -148,25 +148,29 @@ const report = buildDossierReport({
|
|||
unit: THEME_UNIT,
|
||||
narrative,
|
||||
})
|
||||
const bulletin = buildBulletin({
|
||||
facts: report.bulletinFacts,
|
||||
identity: {
|
||||
name: report.meta.subjectName,
|
||||
badges: report.meta.identityBadges,
|
||||
value: report.meta.identity?.value ?? null,
|
||||
taxonomyNote: report.meta.identity?.taxonomyNote ?? null,
|
||||
},
|
||||
unit: THEME_UNIT,
|
||||
generatedAt,
|
||||
})
|
||||
const bulletinIdentity = {
|
||||
name: report.meta.subjectName,
|
||||
badges: report.meta.identityBadges,
|
||||
value: report.meta.identity?.value ?? null,
|
||||
taxonomyNote: report.meta.identity?.taxonomyNote ?? null,
|
||||
}
|
||||
const buildThemeBulletin = (printLimit = null) =>
|
||||
buildBulletin({
|
||||
facts: report.bulletinFacts,
|
||||
identity: bulletinIdentity,
|
||||
unit: THEME_UNIT,
|
||||
generatedAt,
|
||||
printLimit,
|
||||
})
|
||||
|
||||
let bulletin = buildThemeBulletin()
|
||||
let bulletinHtml = renderBulletinHtml(bulletin)
|
||||
|
||||
const dossierMd = renderDossierMd(report)
|
||||
const bulletinHtml = renderBulletinHtml(bulletin)
|
||||
|
||||
const baseName = `${slug}-${generatedAt.toISOString().slice(0, 10)}`
|
||||
|
||||
await mkdir(resolve(ROOT, CACHE_DIR), { recursive: true })
|
||||
await writeFile(resolve(ROOT, join(CACHE_DIR, `${baseName}.boletim.html`)), bulletinHtml)
|
||||
|
||||
await mkdir(resolve(ROOT, outDir), { recursive: true })
|
||||
const dossierMdFile = join(outDir, `${baseName}-dossie.md`)
|
||||
|
|
@ -229,7 +233,18 @@ try {
|
|||
bulletinHtml,
|
||||
dossierPdf: resolve(ROOT, dossierPdfFile),
|
||||
bulletinPdf: resolve(ROOT, bulletinPdfFile),
|
||||
// One-page fit by measurement (C188/C190): while the boletim overflows,
|
||||
// rebuild it with two fewer printed facts — the source panel and the facts
|
||||
// beyond the cap stay counted in the "e mais N" line, never truncated.
|
||||
onBulletinOverflow: async () => {
|
||||
const next = bulletin.factsPrinted - 2
|
||||
if (bulletin.factsPrinted === 0 || next < 0) return null
|
||||
bulletin = buildThemeBulletin(next)
|
||||
bulletinHtml = renderBulletinHtml(bulletin)
|
||||
return bulletinHtml
|
||||
},
|
||||
})
|
||||
await writeFile(resolve(ROOT, join(CACHE_DIR, `${baseName}.boletim.html`)), bulletinHtml)
|
||||
} catch (error) {
|
||||
die(error instanceof Error ? error.message : String(error))
|
||||
} finally {
|
||||
|
|
|
|||
|
|
@ -19,6 +19,14 @@ export const A4_WIDTH_PX = Math.round(210 * MM_TO_PX)
|
|||
export const PAGE_FIT_TOLERANCE_PX = 10
|
||||
export const A4_PAGE_BUDGET_PX = A4_HEIGHT_PX + PAGE_FIT_TOLERANCE_PX
|
||||
|
||||
/**
|
||||
* Bounded re-renders of the one-page boletim while an `onBulletinOverflow`
|
||||
* callback keeps handing back a shorter document (C190). Each pass drops at
|
||||
* least one printed fact, so the cap only exists to fail closed on a callback
|
||||
* that stops shrinking.
|
||||
*/
|
||||
const MAX_BULLETIN_FIT_ATTEMPTS = 24
|
||||
|
||||
export const launchPdfBrowser = () => chromium.launch()
|
||||
|
||||
/** Every `[data-page]` taller than the budget, in document order. */
|
||||
|
|
@ -166,13 +174,19 @@ const printA4Pdf = (page, outPath) =>
|
|||
|
||||
/**
|
||||
* One browser run, two HTML documents: sets each document, asserts its anchor
|
||||
* exists, guards the fit and emits the PDF. Shared by the C186 (city) and C187
|
||||
* (institution) builders — the pages differ, the emit contract does not.
|
||||
* exists, guards the fit and emits the PDF. Shared by the C186 (city), C187
|
||||
* (institution) and C190 (theme) builders — the pages differ, the emit
|
||||
* contract does not.
|
||||
*
|
||||
* C188 fit fallback: when `onDossierOverflow` is given, only the page-1 sheet
|
||||
* (`resumoOnly`) is measured first; if it overflows, the callback rebuilds the
|
||||
* document (pointer lines, never "…") and the fit is checked again — still
|
||||
* fail-closed if even the pointer does not fit.
|
||||
*
|
||||
* C190 bulletin fit: the one-pager is measured and, when `onBulletinOverflow`
|
||||
* is given, the callback rebuilds it with fewer printed facts (the remainder
|
||||
* is declared in the "e mais N" counter) until the A4 holds — still fail-closed
|
||||
* when the callback cannot shrink any further.
|
||||
*/
|
||||
export const emitHtmlPairPdf = async (
|
||||
browser,
|
||||
|
|
@ -182,6 +196,7 @@ export const emitHtmlPairPdf = async (
|
|||
dossierPdf,
|
||||
bulletinPdf,
|
||||
onDossierOverflow = null,
|
||||
onBulletinOverflow = null,
|
||||
resumoOnly = false,
|
||||
},
|
||||
) => {
|
||||
|
|
@ -226,17 +241,35 @@ export const emitHtmlPairPdf = async (
|
|||
}
|
||||
await printA4Pdf(page, dossierPdf)
|
||||
|
||||
await page.setContent(bulletinHtml, { waitUntil: 'load' })
|
||||
let currentBulletinHtml = bulletinHtml
|
||||
await page.setContent(currentBulletinHtml, { waitUntil: 'load' })
|
||||
await page.emulateMedia({ media: 'print' })
|
||||
const hasBulletin = await page.evaluate(() =>
|
||||
Boolean(document.querySelector('[data-page="boletim"]')),
|
||||
)
|
||||
if (!hasBulletin) throw new Error('Bloco do boletim ausente no HTML — renderer quebrado.')
|
||||
assertPageFits(
|
||||
await measurePageOverflows(page, A4_PAGE_BUDGET_PX),
|
||||
'O boletim',
|
||||
'Corte itens/caps — o boletim TEM de caber em uma página.',
|
||||
)
|
||||
for (let attempt = 0; ; attempt += 1) {
|
||||
const hasBulletin = await page.evaluate(() =>
|
||||
Boolean(document.querySelector('[data-page="boletim"]')),
|
||||
)
|
||||
if (!hasBulletin) throw new Error('Bloco do boletim ausente no HTML — renderer quebrado.')
|
||||
const overflows = await measurePageOverflows(page, A4_PAGE_BUDGET_PX)
|
||||
if (overflows.length === 0) break
|
||||
if (!onBulletinOverflow || attempt >= MAX_BULLETIN_FIT_ATTEMPTS) {
|
||||
assertPageFits(
|
||||
overflows,
|
||||
'O boletim',
|
||||
'Corte itens/caps — o boletim TEM de caber em uma página.',
|
||||
)
|
||||
}
|
||||
const rebuilt = await onBulletinOverflow()
|
||||
if (!rebuilt) {
|
||||
assertPageFits(
|
||||
overflows,
|
||||
'O boletim',
|
||||
'Corte itens/caps — o boletim TEM de caber em uma página.',
|
||||
)
|
||||
}
|
||||
currentBulletinHtml = rebuilt
|
||||
await page.setContent(currentBulletinHtml, { waitUntil: 'load' })
|
||||
await page.emulateMedia({ media: 'print' })
|
||||
}
|
||||
await printA4Pdf(page, bulletinPdf)
|
||||
|
||||
await page.close()
|
||||
|
|
|
|||
|
|
@ -419,7 +419,11 @@ const buildMunicipalityReport = ({
|
|||
sourceUrl: row.sourceUrl,
|
||||
sourceDate: row.sourceDate,
|
||||
})),
|
||||
...speechFacts.map((fact) => ({ ...fact, headline: stripInlineSources(fact.headline) })),
|
||||
...speechFacts.map((fact) => ({
|
||||
...fact,
|
||||
headline: stripInlineSources(fact.headline),
|
||||
sourcePanel: true,
|
||||
})),
|
||||
]
|
||||
|
||||
const generatedAtLabel = formatDateTimeBr(generatedAt)
|
||||
|
|
@ -557,7 +561,9 @@ const uncapped = (list) => ({ items: list, total: list.length, omitted: 0 })
|
|||
/**
|
||||
* Speeches from the internal acervo (read-only snapshot) that the research did
|
||||
* not already carry: they are sourced evidence for the recorte, so they feed
|
||||
* the bulletin ledger too — never a second, unsourced fact.
|
||||
* the bulletin ledger too — never a second, unsourced fact. They enter as a
|
||||
* declared source panel (`sourcePanel: true`): counted in the one-pager, never
|
||||
* displacing a finding from the printed slots.
|
||||
*/
|
||||
const speechFactsFromRows = (snapshot, sphere) =>
|
||||
(snapshot.speeches?.rows ?? [])
|
||||
|
|
@ -1068,7 +1074,13 @@ const buildSubjectReport = (params) => {
|
|||
sourceUrl: item.sourceUrl,
|
||||
sourceDate: item.sourceDate,
|
||||
}))
|
||||
.concat(speechFacts.map((fact) => ({ ...fact, headline: stripInlineSources(fact.headline) })))
|
||||
.concat(
|
||||
speechFacts.map((fact) => ({
|
||||
...fact,
|
||||
headline: stripInlineSources(fact.headline),
|
||||
sourcePanel: true,
|
||||
})),
|
||||
)
|
||||
|
||||
const generatedAtLabel = formatDateTimeBr(generatedAt)
|
||||
const readAtLabel = snapshot.meta?.readAt ? formatDateTimeBr(snapshot.meta.readAt) : '—'
|
||||
|
|
|
|||
|
|
@ -30,12 +30,13 @@ const rankFacts = (facts, unit) => {
|
|||
|
||||
/**
|
||||
* @param {{
|
||||
* facts?: Array<{ id: string, era: string, sphere: string, area: string, headline: string, detail: string|null, value: string|null, year: string|null, phase: string|null, sourceUrl: string, sourceDate: string }>,
|
||||
* facts?: Array<{ id: string, era: string, sphere: string, area: string, headline: string, detail: string|null, value: string|null, year: string|null, phase: string|null, sourceUrl: string, sourceDate: string, sourcePanel?: boolean }>,
|
||||
* municipality?: string,
|
||||
* region?: string | null,
|
||||
* identity?: { name: string, badges?: string[] } | null,
|
||||
* unit?: any,
|
||||
* generatedAt?: Date,
|
||||
* printLimit?: number | null,
|
||||
* }} params
|
||||
*/
|
||||
export const buildBulletin = ({
|
||||
|
|
@ -45,11 +46,20 @@ export const buildBulletin = ({
|
|||
identity = null,
|
||||
unit,
|
||||
generatedAt = new Date(),
|
||||
printLimit = null,
|
||||
}) => {
|
||||
const resolvedUnit = resolveDossierUnit(unit)
|
||||
const ordered = rankFacts(facts, resolvedUnit)
|
||||
// The acervo sample is a source panel, not a finding (C190): it is counted in
|
||||
// the one-pager but never takes a printed slot from a sourced finding.
|
||||
const printable = ordered.filter((fact) => !fact.sourcePanel)
|
||||
const target =
|
||||
printLimit === null
|
||||
? BULLETIN_HIGHLIGHT_LIMIT + (resolvedUnit.bulletinMoreLimit ?? BULLETIN_MORE_LIMIT)
|
||||
: Math.max(0, printLimit)
|
||||
const printed = printable.slice(0, target)
|
||||
const subjectName = municipality ?? identity?.name ?? '—'
|
||||
const highlights = ordered.slice(0, BULLETIN_HIGHLIGHT_LIMIT).map((fact) => {
|
||||
const highlights = printed.slice(0, BULLETIN_HIGHLIGHT_LIMIT).map((fact) => {
|
||||
// Theme-only fallback: a fact without money value shows its year; without
|
||||
// either, the card is textual and prints no number slot at all (never zero).
|
||||
const number = resolvedUnit.bulletinNumberFallback
|
||||
|
|
@ -65,20 +75,15 @@ export const buildBulletin = ({
|
|||
phaseLabel: fact.value ? dossierPhaseLabel(fact.phase) : null,
|
||||
}
|
||||
})
|
||||
const moreItems = ordered
|
||||
.slice(
|
||||
BULLETIN_HIGHLIGHT_LIMIT,
|
||||
BULLETIN_HIGHLIGHT_LIMIT + (resolvedUnit.bulletinMoreLimit ?? BULLETIN_MORE_LIMIT),
|
||||
)
|
||||
.map((fact) => ({
|
||||
label: fact.brief?.title ?? fact.headline,
|
||||
detail: fact.brief?.note ?? fact.detail ?? fact.value ?? fact.area,
|
||||
}))
|
||||
const moreItems = printed.slice(BULLETIN_HIGHLIGHT_LIMIT).map((fact) => ({
|
||||
label: fact.brief?.title ?? fact.headline,
|
||||
detail: fact.brief?.note ?? fact.detail ?? fact.value ?? fact.area,
|
||||
}))
|
||||
const shown = highlights.length + moreItems.length
|
||||
|
||||
return {
|
||||
unit: resolvedUnit,
|
||||
sparse: ordered.length < 3,
|
||||
sparse: printable.length < 3,
|
||||
meta: {
|
||||
title: resolvedUnit.bulletinTitle,
|
||||
kicker: resolvedUnit.bulletinKicker ?? 'Boletim informativo · atuação pública',
|
||||
|
|
@ -95,9 +100,12 @@ export const buildBulletin = ({
|
|||
},
|
||||
highlights,
|
||||
moreItems,
|
||||
// Never a silent drop (C188): the facts beyond the two caps are counted so
|
||||
// the one-page boletim states how much of the sourced set it left out.
|
||||
// Never a silent drop (C188): the facts beyond the two caps — and the whole
|
||||
// source panel — are counted so the one-page boletim states how much of the
|
||||
// sourced set it left out.
|
||||
factsTotal: ordered.length,
|
||||
factsPrintable: printable.length,
|
||||
factsPrinted: shown,
|
||||
factsRemaining: ordered.length - shown,
|
||||
timeline: BULLETIN_TIMELINE_STEPS,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -229,6 +229,7 @@ const SUBJECT_BULLETIN_CSS = `
|
|||
.bulletin-timeline { margin-top: 2.5mm; }
|
||||
.more { margin-top: 2.5mm; }
|
||||
.empty-box { min-height: 30mm; }
|
||||
.more-counter { margin-top: 1.8mm; color: #435264; font-size: 7.6pt; font-weight: 600; }
|
||||
.lacuna-panel .warn-icon { width: 5mm; height: 5mm; flex-shrink: 0; margin-top: .4mm; color: #8a5a18; }
|
||||
.highlight-phase { margin: 1.2mm 0 0; }
|
||||
.highlight-card--textual { border-top-color: #435264; }
|
||||
|
|
@ -341,6 +342,11 @@ const renderSubjectBulletinHtml = (bulletin) => {
|
|||
? `<div class="more-grid">${more.map(renderMoreItem).join('')}</div>`
|
||||
: `<div class="empty-box"><p><strong class="uppercase">Sem itens adicionais com fonte além dos destaques.</strong></p><p>${htmlEscape(copy.moreEmpty)}</p></div>`
|
||||
}
|
||||
${
|
||||
bulletin.factsRemaining > 0
|
||||
? `<p class="more-counter">${htmlEscape(moreItemsLabel(bulletin.factsRemaining, 'fato com fonte', 'fatos com fonte'))} ${htmlEscape(copy.remaining)}</p>`
|
||||
: ''
|
||||
}
|
||||
</div>
|
||||
${
|
||||
sparse
|
||||
|
|
|
|||
|
|
@ -206,6 +206,7 @@ export const INSTITUTION_UNIT = {
|
|||
defeso:
|
||||
'fase acompanha cada valor · instituição, setor e rede não são somados · sem percentual estadual absoluto.',
|
||||
orig: 'Conteúdo selecionado do dossiê institucional.',
|
||||
remaining: 'no dossiê da instituição — o boletim de uma página não os exibe.',
|
||||
sparseRule:
|
||||
'Não repetir item, não ampliar efeito e não preencher com ação de setor ou rede como se fosse entrega exclusiva da instituição.',
|
||||
title: 'Uma trajetória de compromisso com o serviço público',
|
||||
|
|
@ -411,6 +412,7 @@ export const THEME_UNIT = {
|
|||
defeso:
|
||||
'fase acompanha cada valor · área, segmento e rede não são somados · sem percentual estadual absoluto.',
|
||||
orig: 'Conteúdo selecionado do dossiê da área.',
|
||||
remaining: 'no dossiê da área — o boletim de uma página não os exibe.',
|
||||
sparseRule:
|
||||
'Não repetir item, não ampliar efeito e não preencher com ação de segmento ou rede como se fosse entrega exclusiva da área.',
|
||||
title: 'Uma trajetória de compromisso com o serviço público',
|
||||
|
|
|
|||
|
|
@ -516,3 +516,48 @@ describe('buildBulletin (theme unit)', () => {
|
|||
expect(sparseHtml).toContain('entrega exclusiva da área')
|
||||
})
|
||||
})
|
||||
|
||||
describe('buildBulletin (theme unit, one-page fit)', () => {
|
||||
const identity = {
|
||||
name: report.meta.subjectName,
|
||||
badges: report.meta.identityBadges,
|
||||
value: report.meta.identity?.value ?? null,
|
||||
taxonomyNote: report.meta.identity?.taxonomyNote ?? null,
|
||||
}
|
||||
const fala = report.bulletinFacts.find((fact) => String(fact.id).startsWith('fala-'))
|
||||
|
||||
it('marks the acervo as a declared source panel, out of the printed slots', () => {
|
||||
expect(fala).toMatchObject({ sourcePanel: true })
|
||||
const bulletin = buildBulletin({
|
||||
facts: fala ? [fala] : [],
|
||||
identity,
|
||||
unit: THEME_UNIT,
|
||||
generatedAt,
|
||||
})
|
||||
expect(bulletin.highlights).toHaveLength(0)
|
||||
expect(bulletin.moreItems).toHaveLength(0)
|
||||
expect(bulletin.factsTotal).toBe(1)
|
||||
expect(bulletin.factsPrinted).toBe(0)
|
||||
expect(bulletin.factsRemaining).toBe(1)
|
||||
expect(bulletin.sparse).toBe(true)
|
||||
const html = renderBulletinHtml(bulletin)
|
||||
expect(html).not.toContain(fala?.headline ?? '')
|
||||
expect(html).toContain('e mais 1 fato com fonte no dossiê da área')
|
||||
})
|
||||
|
||||
it('shrinks the printed set on printLimit and declares every remaining fact', () => {
|
||||
const facts = report.bulletinFacts.filter((fact) => !fact.sourcePanel)
|
||||
const full = buildBulletin({ facts, identity, unit: THEME_UNIT, generatedAt })
|
||||
const fitted = buildBulletin({ facts, identity, unit: THEME_UNIT, generatedAt, printLimit: 2 })
|
||||
expect(full.highlights).toHaveLength(6)
|
||||
expect(full.moreItems).toHaveLength(0)
|
||||
expect(fitted.highlights).toHaveLength(2)
|
||||
expect(fitted.moreItems).toHaveLength(0)
|
||||
expect(fitted.factsPrinted).toBe(2)
|
||||
expect(fitted.factsPrintable).toBe(facts.length)
|
||||
expect(fitted.factsRemaining).toBeGreaterThan(full.factsRemaining)
|
||||
expect(renderBulletinHtml(fitted)).toContain(
|
||||
`e mais ${fitted.factsRemaining} fatos com fonte no dossiê da área`,
|
||||
)
|
||||
})
|
||||
})
|
||||
|
|
|
|||
Loading…
Reference in a new issue