feat(C163): demografia complementar, atividade econômica e transporte no relatório

This commit is contained in:
Francisco Solla 2026-09-15 09:20:10 -03:00
parent 576fda7f2c
commit 32a1a46fad
6 changed files with 209 additions and 21 deletions

View file

@ -107,6 +107,18 @@ URL por item) e emendas lidas da fonte oficial **em tempo de geração**. Págin
"sourceUrl": "https://…", "sourceDate": "2024-08-14"
}
],
"demography": [ // cor/raça e poder aquisitivo (Censo 2022)
{
"topic": "Cor/raça (Censo 2022)", "detail": "Parda 73,1% …",
"sourceUrl": "https://sidra.ibge.gov.br/tabela/9605", "sourceDate": "2026-09-15"
}
],
"economy": [ // PIB, emprego, atividades (IBGE/RAIS e imprensa)
{ "topic": "PIB e perfil", "detail": "…", "sourceUrl": "https://…", "sourceDate": "…" }
],
"transport": [ // rodovias, aeroporto, portos, ferrovia
{ "topic": "Rodovia federal (BR-101)", "detail": "…", "sourceUrl": "https://…", "sourceDate": "…" }
],
"gaps": [{ "id": "…", "label": "…", "reason": "…" }] // lacunas que o agente já sabe
}
```
@ -127,6 +139,18 @@ mais de uma fonte. O PDF mostra o item como "Emendas — indícios web" na pági
1 e as URLs na seção de fontes. **Nunca somar** indício de região/polo como
emenda da cidade.
**Demografia, economia e transporte (pesquisa):** `demography`, `economy` e
`transport` são listas de `{topic, detail, sourceUrl, sourceDate}` (sem fonte →
lacuna). Pesquise: **cor/raça e rendimento** (Censo 2022 — SIDRA 9605 e 10295;
IBGE Cidades para salário médio); **economia** (PIB e PIB per capita — IBGE
Cidades; composição setorial e emprego formal — RAIS/CAGED e perfis regionais;
café, pecuária e comércio no caso do Extremo Sul); **transporte** (rodovias
federais/estaduais que cortam o município e obras novas/reformadas — DNIT,
Seinfra; aeroporto mais próximo em operação e o do polo, com situação; portos e
ferrovia da região — FIOL/Porto Sul, deixando claro quando não serve
diretamente a cidade). O enquadramento é "onde falta × onde o estado e a União
têm acertado" — o candidato é da base.
**Emendas oficiais:** sem `--emendas`, o builder consulta o Portal da
Transparência (`PORTAL_TRANSPARENCIA_API_KEY` no ambiente; sem chave → lacuna).
A API oficial **não filtra por município** (só UF/Nacional/Múltiplo na
@ -148,12 +172,14 @@ cacheado em `data/relatorios-cidade/<base>.emendas.json` para replay.
e estadual)` — top 5 por votos de 2022 na base TSE, com série 2014/2018/2022
e os prováveis candidatos do campo do prefeito (pesquisa) · `3. Rede e
lideranças` — inclui as lideranças locais pesquisadas (ex-prefeitos/vices,
vereadores mais votados) · `4. Conjuntura` · `5. Sinais` · `6. Demandas e
visitas` · `7. Demografia` · `8. Acervo de falas` — cada fala com "O que é"
(sumário oficial) e "Menção ao município" (passagem que cita a cidade ou, se
o nome não aparece nos trechos, a marcação do acervo com o nº de municípios)
· `9. Notícias e imprensa` · `10. Panorama regional` · `11. Abordagem
sugerida (personas)` · `12. Fontes e limites`.
vereadores mais votados, com partido) · `4. Conjuntura` · `5. Sinais` · `6.
Demandas e visitas` · `7. Demografia` — IBGE Censo 2022 do artefato +
**complemento pesquisado** (cor/raça e poder aquisitivo) · `8. Atividade
econômica (pesquisa)` · `9. Transporte e conexões (pesquisa)` · `10. Acervo
de falas` — cada fala com "O que é" (sumário oficial) e "Menção ao município"
(passagem que cita a cidade ou, se o nome não aparece nos trechos, a marcação
do acervo com o nº de municípios) · `11. Notícias e imprensa` · `12. Panorama
regional` · `13. Abordagem sugerida (personas)` · `14. Fontes e limites`.
- **Links clicáveis:** todo URL no PDF é um link (`<a href>`): células de
tabela, fontes por linha e a seção de fontes.

View file

@ -0,0 +1 @@
**C163 — demografia complementar, economia e transporte no relatório (2026-09-15):** a seção de Demografia ganhou o complemento pesquisado de **cor/raça e poder aquisitivo** (Censo 2022; Itamaraju: parda 73,1%, branca 13,7%, preta 11,1%, indígena 2,0%; renda domiciliar per capita média R$ 997,27 e mediana R$ 725,00) e entraram duas seções novas: **"8. Atividade econômica (pesquisa)"** (PIB e per capita, composição setorial, emprego formal e atividades — IBGE Cidades, RAIS/perfis regionais; Itamaraju: PIB per capita R$ 23.412,56 em 2023, serviços 38,2%, agropecuária 27,7%, administração pública 23,7%, indústria 10,3%) e **"9. Transporte e conexões (pesquisa)"** (rodovias que cortam o município e obras novas/reformadas, aeroporto mais próximo em operação e o do polo, ferrovia/portos da região — FIOL/Porto Sul com a ressalva de não servir diretamente a cidade). O enquadramento é "onde falta × onde o estado e a União têm acertado", com URL por item. Contrato: `demography`, `economy` e `transport` (`{topic, detail, sourceUrl, sourceDate}`) validados — sem fonte vira lacuna —, listados nas fontes e documentados na skill; seções renumeradas para 14. Unit 3118/3118, `tsc`/eslint verdes; PDF de Itamaraju em 9 páginas.

View file

@ -747,7 +747,43 @@ const buildDemandsVisitsSection = ({ snapshot }) => {
return blocks
}
const buildDemographicsSection = ({ snapshot }) => {
const buildSourcedListBlocks = ({ title, note, items }) => {
if (!items.length) {
return [
{
kind: 'callout',
tone: 'gap',
title,
body: ['Nenhum item pesquisado com fonte para este município.', GAP_COPY],
},
]
}
return [
{
kind: 'table',
title,
columns: [
{ key: 'topic', label: 'Tema', width: 22 },
{ key: 'detail', label: 'Dado', width: 62 },
{ key: 'source', label: 'Fonte', width: 16 },
],
rows: items.map((item) => ({
topic: item.topic,
detail: item.detail,
source: formatDateBr(item.sourceDate),
})),
note,
sources: items.map((item) => ({
kind: 'web',
label: item.topic,
url: item.sourceUrl,
date: item.sourceDate,
})),
},
]
}
const buildDemographicsSection = ({ snapshot, research }) => {
const demographics = snapshot.demographics
if (!demographics) {
return [
@ -759,10 +795,10 @@ const buildDemographicsSection = ({ snapshot }) => {
},
]
}
return [
const blocks = [
{
kind: 'stats',
title: 'Demografia',
title: 'Demografia (IBGE Censo 2022)',
rows: [
{ label: 'População', value: formatInteger(demographics.population) },
{ label: '0–17 anos', value: formatInteger(demographics.ageBands['0-17']) },
@ -779,8 +815,30 @@ const buildDemographicsSection = ({ snapshot }) => {
sources: [sourceTeqo('IBGE Censo 2022 (artefato commitado)', null)],
},
]
blocks.push(
...buildSourcedListBlocks({
title: 'Demografia complementar (pesquisa)',
note: 'Cor/raça e poder aquisitivo (IBGE Censo 2022) — leitura para agenda e linguagem na cidade; cada item com URL.',
items: research.demography ?? [],
}),
)
return blocks
}
const buildEconomySection = ({ research }) =>
buildSourcedListBlocks({
title: 'Atividade econômica (pesquisa)',
note: 'PIB, emprego e principais atividades (IBGE/CAGED e imprensa local) — onde a cidade gera renda e onde depende de fora; cada item com URL.',
items: research.economy ?? [],
})
const buildTransportSection = ({ research }) =>
buildSourcedListBlocks({
title: 'Transporte e conexões (pesquisa)',
note: 'Rodovias (novas/reformadas), aeroporto mais próximo, portos e ferrovia: como a cidade se conecta à Bahia e o que está em obra/planejado — cada item com URL.',
items: research.transport ?? [],
})
const buildSpeechesSection = ({ snapshot }) => {
const { speeches, municipality } = snapshot
if (!speeches.rows.length) {
@ -996,6 +1054,16 @@ const buildSourcesSection = ({ snapshot, research, emendas }) => {
date: item.sourceDate,
})
}
for (const listKey of ['demography', 'economy', 'transport']) {
for (const item of research[listKey] ?? []) {
items.push({
kind: 'web',
label: `${listKey === 'demography' ? 'Demografia' : listKey === 'economy' ? 'Economia' : 'Transporte'} — ${item.topic}`,
url: item.sourceUrl,
date: item.sourceDate,
})
}
}
return [
{
@ -1069,26 +1137,40 @@ export const buildCityReport = ({ snapshot, research, emendas, generatedAt = new
title: '6. Demandas e visitas',
blocks: buildDemandsVisitsSection({ snapshot }),
},
{ id: 'demografia', title: '7. Demografia', blocks: buildDemographicsSection({ snapshot }) },
{ id: 'falas', title: '8. Acervo de falas', blocks: buildSpeechesSection({ snapshot }) },
{
id: 'demografia',
title: '7. Demografia',
blocks: buildDemographicsSection({ snapshot, research }),
},
{
id: 'economia',
title: '8. Atividade econômica (pesquisa)',
blocks: buildEconomySection({ research }),
},
{
id: 'transporte',
title: '9. Transporte e conexões (pesquisa)',
blocks: buildTransportSection({ research }),
},
{ id: 'falas', title: '10. Acervo de falas', blocks: buildSpeechesSection({ snapshot }) },
{
id: 'noticias',
title: '9. Notícias internas e imprensa local',
title: '11. Notícias internas e imprensa local',
blocks: buildNewsSection({ research }),
},
{
id: 'regiao',
title: '10. Panorama regional (Território de Identidade)',
title: '12. Panorama regional (Território de Identidade)',
blocks: buildRegionSection({ snapshot }),
},
{
id: 'abordagem',
title: '11. Abordagem sugerida (pesquisa)',
title: '13. Abordagem sugerida (pesquisa)',
blocks: buildApproachSection({ research }),
},
{
id: 'fontes',
title: '12. Fontes e limites',
title: '14. Fontes e limites',
blocks: buildSourcesSection({ snapshot, research, emendas }),
},
]

View file

@ -267,6 +267,39 @@ export const normalizeResearchInput = (raw, { now = new Date() } = {}) => {
})
}
const sourcedLists = { demography: [], economy: [], transport: [] }
for (const key of Object.keys(sourcedLists)) {
for (const entry of Array.isArray(raw[key]) ? raw[key] : []) {
const topic = isNonEmptyString(entry?.topic) ? entry.topic.trim() : null
const detail = isNonEmptyString(entry?.detail) ? entry.detail.trim() : null
const sourceUrl = isNonEmptyString(entry?.sourceUrl) ? entry.sourceUrl.trim() : null
const sourceDate = isValidDate(entry?.sourceDate) ? entry.sourceDate : null
if (!topic || !detail) {
gaps.push({
id: `${key}_incompleto`,
label: topic,
reason: 'Item sem tema ou dado — descartado.',
})
continue
}
if (!sourceUrl || !sourceDate) {
gaps.push({
id: `${key}_sem_fonte`,
label: topic,
reason: 'Item sem fonte: URL e data são obrigatórias.',
})
continue
}
sourcedLists[key].push({
topic,
detail,
sourceUrl,
sourceDate,
consultedAt: isValidDate(entry.consultedAt) ? entry.consultedAt : null,
})
}
}
return {
municipalitySlug: raw.municipalitySlug.trim(),
researchedAt: researchedAt.toISOString(),
@ -275,6 +308,9 @@ export const normalizeResearchInput = (raw, { now = new Date() } = {}) => {
approach,
preCandidates,
leaders,
demography: sourcedLists.demography,
economy: sourcedLists.economy,
transport: sourcedLists.transport,
gaps,
}
}

View file

@ -70,6 +70,30 @@ const research = normalizeResearchInput(
sourceDate: '2026-03-05',
},
],
demography: [
{
topic: 'Cor/raça',
detail: 'Parda 62%, branca 25%.',
sourceUrl: 'https://exemplo.test/cor-raca',
sourceDate: '2026-09-10',
},
],
economy: [
{
topic: 'PIB',
detail: 'Serviços e agropecuária lideram.',
sourceUrl: 'https://exemplo.test/pib',
sourceDate: '2026-09-10',
},
],
transport: [
{
topic: 'Rodovias',
detail: 'BR-101 corta o município; trecho em obra.',
sourceUrl: 'https://exemplo.test/br101',
sourceDate: '2026-09-10',
},
],
gaps: [],
},
{ now: generatedAt },
@ -323,12 +347,29 @@ const asReport = (value: unknown) => value as ReportShape
describe('buildCityReport', () => {
const report = asReport(buildCityReport({ snapshot, research, emendas, generatedAt }))
it('builds the 12 deepening sections in the product order', () => {
expect(report.sections).toHaveLength(12)
it('builds the 14 deepening sections in the product order', () => {
expect(report.sections).toHaveLength(14)
expect(report.sections[0].title).toMatch(/Conta eleitoral completa/)
expect(report.sections[1].title).toMatch(/Concorrentes no município/)
expect(report.sections[10].title).toMatch(/Abordagem sugerida/)
expect(report.sections[11].title).toMatch(/Fontes e limites/)
expect(report.sections[7].title).toMatch(/Atividade econômica/)
expect(report.sections[8].title).toMatch(/Transporte e conexões/)
expect(report.sections[12].title).toMatch(/Abordagem sugerida/)
expect(report.sections[13].title).toMatch(/Fontes e limites/)
})
it('adds the researched demography, economy and transport data', () => {
const demografia = report.sections.find((section) => section.id === 'demografia')!
const complementary = demografia.blocks.find(
(block) => block.title === 'Demografia complementar (pesquisa)',
)!
expect((complementary.rows as unknown as TableRow[])[0].topic).toBe('Cor/raça')
const economia = report.sections.find((section) => section.id === 'economia')!.blocks[0]
expect((economia.rows as unknown as TableRow[])[0].detail).toContain('Serviços')
const transporte = report.sections.find((section) => section.id === 'transporte')!.blocks[0]
expect((transporte.rows as unknown as TableRow[])[0].detail).toContain('BR-101')
expect(transporte.sources![0]).toEqual(
expect.objectContaining({ url: 'https://exemplo.test/br101' }),
)
})
it('lists the main competitors with their vote series and the pre-candidates', () => {

View file

@ -114,8 +114,10 @@ describe('renderReportMd', () => {
it('carries the same sections as tables and headings', () => {
expect(md).toContain('## 1. Conta eleitoral completa')
expect(md).toContain('## 2. Concorrentes no município (federal e estadual)')
expect(md).toContain('## 11. Abordagem sugerida (pesquisa)')
expect(md).toContain('## 12. Fontes e limites')
expect(md).toContain('## 8. Atividade econômica (pesquisa)')
expect(md).toContain('## 9. Transporte e conexões (pesquisa)')
expect(md).toContain('## 13. Abordagem sugerida (pesquisa)')
expect(md).toContain('## 14. Fontes e limites')
expect(md).toContain('| Ano | Votos |')
})