diff --git a/.agents/skills/relatorio-cidade/SKILL.md b/.agents/skills/relatorio-cidade/SKILL.md
index 3e18387d..7401ce2e 100644
--- a/.agents/skills/relatorio-cidade/SKILL.md
+++ b/.agents/skills/relatorio-cidade/SKILL.md
@@ -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/.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 (``): células de
tabela, fontes por linha e a seção de fontes.
diff --git a/docs/changelog/2026-09-15-c163-economia-transporte-diversidade.md b/docs/changelog/2026-09-15-c163-economia-transporte-diversidade.md
new file mode 100644
index 00000000..64d2764b
--- /dev/null
+++ b/docs/changelog/2026-09-15-c163-economia-transporte-diversidade.md
@@ -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.
diff --git a/scripts/lib/cityReportBlocks.mjs b/scripts/lib/cityReportBlocks.mjs
index f31e556a..20f93e75 100644
--- a/scripts/lib/cityReportBlocks.mjs
+++ b/scripts/lib/cityReportBlocks.mjs
@@ -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 }),
},
]
diff --git a/scripts/lib/cityReportResearch.mjs b/scripts/lib/cityReportResearch.mjs
index 4a875ed3..90ad7c5e 100644
--- a/scripts/lib/cityReportResearch.mjs
+++ b/scripts/lib/cityReportResearch.mjs
@@ -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,
}
}
diff --git a/tests/unit/cityReportBlocks.unit.spec.ts b/tests/unit/cityReportBlocks.unit.spec.ts
index f462c07d..33486ad4 100644
--- a/tests/unit/cityReportBlocks.unit.spec.ts
+++ b/tests/unit/cityReportBlocks.unit.spec.ts
@@ -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', () => {
diff --git a/tests/unit/cityReportRender.unit.spec.ts b/tests/unit/cityReportRender.unit.spec.ts
index 0edeafec..d0d467ba 100644
--- a/tests/unit/cityReportRender.unit.spec.ts
+++ b/tests/unit/cityReportRender.unit.spec.ts
@@ -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 |')
})